From 2b8752a3fc12c27373ae9cbb6e312b8a10a655e2 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Sun, 23 Mar 2025 15:54:09 +0100 Subject: [PATCH] Propfind partial implementation --- crates/common/src/core.rs | 26 +- crates/common/src/lib.rs | 3 +- crates/dav/src/common/lock.rs | 156 +++++---- crates/dav/src/common/mod.rs | 10 +- crates/dav/src/common/uri.rs | 49 +++ crates/dav/src/file/changes.rs | 29 -- crates/dav/src/file/copy_move.rs | 6 +- crates/dav/src/file/delete.rs | 3 +- crates/dav/src/file/get.rs | 2 +- crates/dav/src/file/mkcol.rs | 3 +- crates/dav/src/file/mod.rs | 1 - crates/dav/src/file/propfind.rs | 485 +++++++++++++++++++++------- crates/dav/src/file/proppatch.rs | 2 +- crates/dav/src/file/update.rs | 5 +- crates/dav/src/request.rs | 28 +- crates/email/src/mailbox/destroy.rs | 99 +++--- crates/email/src/message/delete.rs | 125 +++---- crates/imap/src/core/mailbox.rs | 55 ++-- crates/imap/src/core/message.rs | 58 ++-- crates/imap/src/op/expunge.rs | 94 +++--- crates/pop3/src/mailbox.rs | 35 +- 21 files changed, 801 insertions(+), 473 deletions(-) delete mode 100644 crates/dav/src/file/changes.rs diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index 69d4491c..60524144 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -18,8 +18,8 @@ use store::{ dispatch::DocumentSet, roaring::RoaringBitmap, write::{ - BatchBuilder, BitmapClass, BlobOp, DirectoryClass, QueueClass, TagValue, ValueClass, - key::DeserializeBigEndian, log::ChangeLogBuilder, now, + AlignedBytes, Archive, BatchBuilder, BitmapClass, BlobOp, DirectoryClass, QueueClass, + TagValue, ValueClass, key::DeserializeBigEndian, log::ChangeLogBuilder, now, }, }; use trc::AddContext; @@ -380,21 +380,20 @@ impl Server { }) } - pub async fn get_properties( + pub async fn get_archives( &self, account_id: u32, collection: Collection, - iterate: &I, + documents: &I, property: Property, - ) -> trc::Result> + mut cb: CB, + ) -> trc::Result<()> where I: DocumentSet + Send + Sync, - U: Deserialize + 'static, + CB: FnMut(u32, Archive) -> trc::Result + Send + Sync, { let property: u8 = property.as_ref().into(); let collection: u8 = collection.into(); - let expected_results = iterate.len(); - let mut results = Vec::with_capacity(expected_results); self.core .storage @@ -404,21 +403,21 @@ impl Server { ValueKey { account_id, collection, - document_id: iterate.min(), + document_id: documents.min(), class: ValueClass::Property(property), }, ValueKey { account_id, collection, - document_id: iterate.max(), + document_id: documents.max(), class: ValueClass::Property(property), }, ), |key, value| { let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; - if iterate.contains(document_id) { - results.push((document_id, U::deserialize(value)?)); - Ok(expected_results == 0 || results.len() < expected_results) + if documents.contains(document_id) { + as Deserialize>::deserialize(value) + .and_then(|archive| cb(document_id, archive)) } else { Ok(true) } @@ -431,7 +430,6 @@ impl Server { .collection(collection) .id(property.to_string()) }) - .map(|_| results) } pub async fn get_document_ids( diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 449b1f8c..dc00ea1a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -41,7 +41,6 @@ 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; @@ -103,7 +102,7 @@ pub const KV_LOCK_QUEUE_REPORT: u8 = 22; pub const KV_LOCK_EMAIL_TASK: u8 = 23; pub const KV_LOCK_HOUSEKEEPER: u8 = 24; pub const KV_LOCK_DAV: u8 = 25; -pub const KV_SIEVE_ID: u8 = 27; +pub const KV_SIEVE_ID: u8 = 26; #[derive(Clone)] pub struct Server { diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 4af2c5ed..c2862ec9 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -25,16 +25,17 @@ use store::{SERIALIZE_OBJ_02_V1, Serialize, SerializedVersion, U32_LEN}; use trc::AddContext; use super::ETag; -use super::uri::{DavUriResource, OwnedUri}; +use super::uri::{DavUriResource, OwnedUri, Urn}; use crate::{DavError, DavErrorCondition, DavMethod}; -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct ResourceState<'x> { pub account_id: u32, pub collection: Collection, pub document_id: Option, pub etag: Option, pub lock_token: Option, + pub sync_token: Option, pub path: &'x str, } @@ -80,9 +81,7 @@ impl LockRequestHandler for Server { account_id, collection: resource.collection, path: resource_path, - document_id: None, - etag: None, - lock_token: None, + ..Default::default() }]; let mut lock_data = if let Some(lock_data) = self @@ -174,12 +173,14 @@ impl LockRequestHandler for Server { .to_string(), ) } else { - let lock_token = headers + let (lock_expires, lock_id) = headers .lock_token + .and_then(Urn::parse) + .and_then(|urn| urn.try_unwrap_lock()) .ok_or(DavError::Code(StatusCode::BAD_REQUEST))?; let mut found_path = None; for (lock_path, lock_item) in lock_data.locks.iter() { - if lock_item.uuid() == lock_token { + if lock_item.expires == lock_expires && lock_item.lock_id == lock_id { if lock_item.is_lock_owner(access_token) { found_path = Some(lock_path.to_string()); break; @@ -273,7 +274,7 @@ impl LockRequestHandler for Server { // Unarchive lock data let mut locks = locks_.to_unarchived().caused_by(trc::location!())?; - // Validate locks + // Validate locks for write operations if !matches!(method, DavMethod::GET | DavMethod::HEAD) { for resource in &resources { if let Some(idx) = locks.find_cache_pos(self, resource).await? { @@ -300,10 +301,8 @@ impl LockRequestHandler for Server { let mut resource_not_found = ResourceState { account_id: u32::MAX, collection: Collection::None, - document_id: None, - etag: None, - lock_token: None, path: "", + ..Default::default() }; 'outer: for if_ in &headers.if_ { @@ -323,9 +322,7 @@ impl LockRequestHandler for Server { account_id: r.account_id?, collection: r.collection, path: r.resource?, - document_id: None, - etag: None, - lock_token: None, + ..Default::default() }) }) { @@ -346,15 +343,27 @@ impl LockRequestHandler for Server { // Fill missing data for resource if resource_state.collection != Collection::None - && (resource_state.etag.is_none() || resource_state.lock_token.is_none()) + && (resource_state.etag.is_none() + || resource_state.lock_token.is_none() + || resource_state.sync_token.is_none()) { - let mut needs_token = false; + let mut needs_lock_token = false; + let mut needs_sync_token = false; let mut needs_etag = false; for cond in &if_.list { match cond { - Condition::StateToken { .. } => { - needs_token = true; + Condition::StateToken { token, .. } => { + match Urn::parse(token) + .ok_or(DavError::Code(StatusCode::BAD_REQUEST))? + { + Urn::Lock { .. } => { + needs_lock_token = true; + } + Urn::Sync { .. } => { + needs_sync_token = true; + } + } } Condition::ETag { .. } | Condition::Exists { .. } => { needs_etag = true; @@ -404,18 +413,35 @@ impl LockRequestHandler for Server { } // Fetch lock token - if needs_token && resource_state.lock_token.is_none() { + if needs_lock_token && resource_state.lock_token.is_none() { if let Some(idx) = locks.find_cache_pos(self, resource_state).await? { if let Some((_, lock)) = locks.find_lock_by_pos(idx, resource_state)? { - resource_state.lock_token = Some(lock.uuid()); + resource_state.lock_token = Some(lock.urn().to_string()); } } } + + // Fetch sync token + if needs_sync_token && resource_state.sync_token.is_none() { + let change_id = self + .store() + .get_last_change_id(resource_state.account_id, resource_state.collection) + .await + .caused_by(trc::location!())?; + resource_state.sync_token = Some( + Urn::Sync { + id: change_id.unwrap_or_default(), + } + .to_string(), + ); + } } for cond in &if_.list { match cond { - Condition::StateToken { is_not, token } => { + Condition::StateToken { is_not, token } + if token.starts_with("urn:stalwart:davlock:") => + { if !((resource_state .lock_token .as_ref() @@ -425,6 +451,18 @@ impl LockRequestHandler for Server { continue 'outer; } } + Condition::StateToken { is_not, token } + if token.starts_with("urn:stalwart:davsync:") => + { + if !((resource_state + .sync_token + .as_ref() + .is_some_and(|sync_token| sync_token == token)) + ^ is_not) + { + continue 'outer; + } + } Condition::ETag { is_not, tag } => { if !((resource_state.etag.as_ref().is_some_and(|etag| etag == tag)) ^ is_not) @@ -437,6 +475,9 @@ impl LockRequestHandler for Server { continue 'outer; } } + _ => { + return Err(DavError::Code(StatusCode::BAD_REQUEST)); + } } } @@ -569,12 +610,12 @@ impl<'x> LockCaches<'x> { } #[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] -struct LockData { +pub(crate) struct LockData { locks: HashMap, } #[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] -struct LockItem { +pub(crate) struct LockItem { lock_id: u64, owner: u32, expires: u64, @@ -590,6 +631,11 @@ impl SerializedVersion for LockData { } impl LockItem { + #[inline] + pub fn is_lock_owner(&self, access_token: &AccessToken) -> bool { + self.owner == access_token.primary_id + } + pub fn to_active_lock(&self, href: String) -> ActiveLock { ActiveLock::new( href, @@ -606,25 +652,14 @@ impl LockItem { }) .with_owner_opt(self.owner_dav.clone()) .with_timeout(self.expires.saturating_sub(now())) - .with_lock_token(self.uuid()) + .with_lock_token(self.urn().to_string()) } - pub fn uuid(&self) -> String { - let lock_id_high = (self.lock_id >> 32) as u32; - let lock_id_low = self.lock_id as u32; - let expires_high = (self.expires >> 48) as u16; - let expires_low = ((self.expires >> 16) & 0xFFFF) as u16; - - format!( - "urn:uuid:{:08x}-{:04x}-{:04x}-{:04x}-{:04x}{:04x}{:04x}", - lock_id_high, - lock_id_low >> 16, - lock_id_low & 0xFFFF, - self.owner >> 16, - self.owner & 0xFFFF, - expires_high, - expires_low - ) + pub fn urn(&self) -> Urn { + Urn::Lock { + expires: self.expires, + id: self.lock_id, + } } } @@ -675,29 +710,30 @@ impl ArchivedLockItem { self.owner == access_token.primary_id } - pub fn uuid(&self) -> String { - let lock_id_high = (self.lock_id >> 32) as u32; - let lock_id_low = u64::from(self.lock_id) as u32; - let expires_high = (self.expires >> 48) as u16; - let expires_low = ((self.expires >> 16) & 0xFFFF) as u16; - - format!( - "urn:uuid:{:08x}-{:04x}-{:04x}-{:04x}-{:04x}{:04x}{:04x}", - lock_id_high, - lock_id_low >> 16, - lock_id_low & 0xFFFF, - self.owner >> 16, - self.owner & 0xFFFF, - expires_high, - expires_low + pub fn to_active_lock(&self, href: String) -> ActiveLock { + ActiveLock::new( + href, + if self.exclusive { + LockScope::Exclusive + } else { + LockScope::Shared + }, ) + .with_depth(if self.depth_infinity { + Depth::Infinity + } else { + Depth::Zero + }) + .with_owner_opt(self.owner_dav.as_ref().map(Into::into)) + .with_timeout(u64::from(self.expires).saturating_sub(now())) + .with_lock_token(self.urn().to_string()) } -} -impl LockItem { - #[inline] - pub fn is_lock_owner(&self, access_token: &AccessToken) -> bool { - self.owner == access_token.primary_id + pub fn urn(&self) -> Urn { + Urn::Lock { + expires: self.expires.into(), + id: self.lock_id.into(), + } } } diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index 41ebd83a..bed151cb 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -13,7 +13,7 @@ use store::{ U32_LEN, write::{Archive, BatchBuilder, MaybeDynamicValue, Operation, ValueClass, ValueOp}, }; -use uri::OwnedUri; +use uri::{OwnedUri, Urn}; pub mod acl; pub mod lock; @@ -96,7 +96,13 @@ impl<'x> DavQuery<'x> { resource, propfind: changes.properties, base_uri: headers.base_uri().unwrap_or_default(), - from_change_id: changes.sync_token.and_then(|s| s.parse().ok()), + from_change_id: changes + .sync_token + .as_deref() + .and_then(Urn::parse) + .and_then(|urn| urn.try_unwrap_sync()) + .unwrap_or_default() + .into(), depth: if changes.level_inf { usize::MAX } else { 1 }, limit: changes.limit, ret: headers.ret, diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index 1d2f3670..4bb41e39 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use std::fmt::Display; + use common::{Server, auth::AccessToken}; use directory::backend::internal::manage::ManageDirectory; @@ -20,6 +22,11 @@ pub(crate) struct UriResource { pub resource: R, } +pub(crate) enum Urn { + Lock { expires: u64, id: u64 }, + Sync { id: u64 }, +} + pub(crate) type UnresolvedUri<'x> = UriResource, Option<&'x str>>; pub(crate) type OwnedUri<'x> = UriResource>; //pub(crate) type DocumentUri<'x> = UriResource; @@ -115,3 +122,45 @@ impl OwnedUri<'_> { } } } + +impl Urn { + pub fn parse(input: &str) -> Option { + let inbox = input.strip_prefix("urn:stalwart:")?; + let (kind, id) = inbox.split_once(':')?; + match kind { + "davlock" => u128::from_str_radix(id, 16).ok().map(|id| Urn::Lock { + expires: (id >> 64) as u64, + id: id as u64, + }), + "davsync" => u64::from_str_radix(id, 16).ok().map(|id| Urn::Sync { id }), + _ => None, + } + } + + pub fn try_unwrap_lock(&self) -> Option<(u64, u64)> { + match self { + Urn::Lock { expires, id } => Some((*expires, *id)), + _ => None, + } + } + + pub fn try_unwrap_sync(&self) -> Option { + match self { + Urn::Sync { id } => Some(*id), + _ => None, + } + } +} + +impl Display for Urn { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Urn::Lock { expires, id } => write!( + f, + "urn:stalwart:davlock:{:x}", + (u128::from(*expires) << 64) | u128::from(*id) + ), + Urn::Sync { id } => write!(f, "urn:stalwart:davsync:{:x}", id), + } + } +} diff --git a/crates/dav/src/file/changes.rs b/crates/dav/src/file/changes.rs deleted file mode 100644 index 27f29d5a..00000000 --- a/crates/dav/src/file/changes.rs +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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::SyncCollection}; -use http_proto::HttpResponse; - -pub(crate) trait FileChangesRequestHandler: Sync + Send { - fn handle_file_changes_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: SyncCollection, - ) -> impl Future> + Send; -} - -impl FileChangesRequestHandler for Server { - async fn handle_file_changes_request( - &self, - access_token: &AccessToken, - headers: RequestHeaders<'_>, - request: SyncCollection, - ) -> crate::Result { - todo!() - } -} diff --git a/crates/dav/src/file/copy_move.rs b/crates/dav/src/file/copy_move.rs index 35c41a01..7a008439 100644 --- a/crates/dav/src/file/copy_move.rs +++ b/crates/dav/src/file/copy_move.rs @@ -199,17 +199,15 @@ impl FileCopyMoveRequestHandler for Server { account_id: from_account_id, collection: Collection::FileNode, document_id: Some(from_resource.resource.document_id), - etag: None, - lock_token: None, path: from_resource_.resource.unwrap(), + ..Default::default() }, ResourceState { account_id: to_account_id, collection: Collection::FileNode, document_id: Some(destination.document_id.unwrap_or(u32::MAX)), - etag: None, - lock_token: None, path: destination_resource_name, + ..Default::default() }, ], Default::default(), diff --git a/crates/dav/src/file/delete.rs b/crates/dav/src/file/delete.rs index f5d7aa06..c3cca3df 100644 --- a/crates/dav/src/file/delete.rs +++ b/crates/dav/src/file/delete.rs @@ -95,9 +95,8 @@ impl FileDeleteRequestHandler for Server { account_id, collection: resource.collection, document_id: document_id.into(), - etag: None, - lock_token: None, path: delete_path, + ..Default::default() }], Default::default(), DavMethod::DELETE, diff --git a/crates/dav/src/file/get.rs b/crates/dav/src/file/get.rs index 3ba0f13c..4d3a579a 100644 --- a/crates/dav/src/file/get.rs +++ b/crates/dav/src/file/get.rs @@ -88,8 +88,8 @@ impl FileGetRequestHandler for Server { collection: resource.collection, document_id: resource.resource.into(), etag: etag.clone().into(), - lock_token: None, path: resource_.resource.unwrap(), + ..Default::default() }], Default::default(), DavMethod::GET, diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 77f93fcf..245a54c4 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -75,9 +75,8 @@ impl FileMkColRequestHandler for Server { account_id, collection: resource.collection, document_id: Some(u32::MAX), - etag: None, - lock_token: None, path: resource_.resource.unwrap(), + ..Default::default() }], Default::default(), DavMethod::MKCOL, diff --git a/crates/dav/src/file/mod.rs b/crates/dav/src/file/mod.rs index 903f9384..2b685d2e 100644 --- a/crates/dav/src/file/mod.rs +++ b/crates/dav/src/file/mod.rs @@ -25,7 +25,6 @@ use crate::{ }; pub mod acl; -pub mod changes; pub mod copy_move; pub mod delete; pub mod get; diff --git a/crates/dav/src/file/propfind.rs b/crates/dav/src/file/propfind.rs index 73dff593..49cb1a53 100644 --- a/crates/dav/src/file/propfind.rs +++ b/crates/dav/src/file/propfind.rs @@ -4,9 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{FileItem, Server, auth::AccessToken}; use dav_proto::schema::{ - property::{DavProperty, WebDavProperty}, + property::{ + DavProperty, DavValue, ReportSet, ResourceType, Rfc1123DateTime, SupportedLock, + WebDavProperty, + }, request::{DavPropertyValue, PropFind}, response::{MultiStatus, PropStat, Response}, }; @@ -25,7 +28,7 @@ use trc::AddContext; use utils::map::bitmap::Bitmap; use crate::{ - common::DavQuery, + common::{DavQuery, ETag, lock::LockData, uri::Urn}, principal::propfind::{PrincipalPropFind, PrincipalResource}, }; @@ -66,7 +69,7 @@ impl HandleFilePropFindRequest for Server { }; // Filter by changelog - let mut last_change_id = None; + let mut sync_token = None; if let Some(change_id) = query.from_change_id { let changelog = self .store() @@ -78,7 +81,12 @@ impl HandleFilePropFindRequest for Server { self.core.dav.max_changes, ); if changelog.to_change_id != 0 { - last_change_id = Some(changelog.to_change_id); + sync_token = Some( + Urn::Sync { + id: changelog.to_change_id, + } + .to_string(), + ); } let mut changes = RoaringBitmap::from_iter(changelog.changes.iter().map(|change| change.id() as u32)); @@ -93,21 +101,16 @@ impl HandleFilePropFindRequest for Server { } 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::>() + let paths = if let Some(resource) = query.resource.resource { + Paths::new( + files + .subtree_with_depth(resource, query.depth) + .filter(|item| { + document_ids + .as_ref() + .is_none_or(|d| d.contains(item.document_id)) + }), + ) } else { if !query.depth_no_root || query.from_change_id.is_none() { self.prepare_principal_propfind_response( @@ -123,20 +126,11 @@ impl HandleFilePropFindRequest for Server { .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::>() + Paths::new(files.tree_with_depth(query.depth - 1).filter(|item| { + document_ids + .as_ref() + .is_none_or(|d| d.contains(item.document_id)) + })) }; if paths.is_empty() && query.from_change_id.is_none() { @@ -153,12 +147,26 @@ impl HandleFilePropFindRequest for Server { let todo = "prefer minimal"; // Prepare response - let (fields, is_all_prop) = match query.propfind { + let (fields, is_all_prop) = match &query.propfind { PropFind::PropName => { - for (_, (path, is_container)) in paths { + for (_, item) in paths.items { + let props = if item.is_container { + FOLDER_PROPS + .iter() + .cloned() + .map(DavPropertyValue::empty) + .collect::>() + } else { + FILE_PROPS + .iter() + .cloned() + .map(DavPropertyValue::empty) + .collect::>() + }; + response.add_response(Response::new_propstat( - path, - vec![PropStat::new_list(all_properties(is_container))], + query.format_to_base_uri(&item.name), + vec![PropStat::new_list(props)], )); } @@ -166,117 +174,350 @@ impl HandleFilePropFindRequest for Server { 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, - ), + PropFind::AllProp(items) => (items, true), + PropFind::Prop(items) => (items, false), }; - for (document_id, node_) in self - .get_properties::, _>( - account_id, - Collection::FileNode, - &Paths(&paths), - Property::Value, - ) - .await - .caused_by(trc::location!())? + // Fetch sync token + if sync_token.is_none() + && (is_all_prop + || query.from_change_id.is_some() + || fields + .iter() + .any(|field| matches!(field, DavProperty::WebDav(WebDavProperty::SyncToken)))) { - 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)], - )); + let id = self + .store() + .get_last_change_id(account_id, Collection::FileNode) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + sync_token = Some(Urn::Sync { id }.to_string()) } + // Add sync token + if query.from_change_id.is_some() { + response = response.with_sync_token(sync_token.clone().unwrap()); + } + + // Fetch locks + #[allow(unused_assignments)] + let mut locks_ = None; + let mut locks = None; + if is_all_prop + || fields + .iter() + .any(|field| matches!(field, DavProperty::WebDav(WebDavProperty::LockDiscovery))) + { + if let Some(lock_archive) = self + .in_memory_store() + .key_get::>(query.resource.lock_key().as_slice()) + .await + .caused_by(trc::location!())? + { + locks_ = Some(lock_archive); + locks = Some( + locks_ + .as_ref() + .unwrap() + .unarchive::() + .caused_by(trc::location!())?, + ); + } + } + + self.get_archives( + account_id, + Collection::FileNode, + &paths, + Property::Value, + |document_id, node_| { + let node = node_.unarchive::().caused_by(trc::location!())?; + let item = paths.items.get(&document_id).unwrap(); + let is_container = node.file.is_none(); + let properties: Box> = if is_all_prop { + Box::new(if is_container { + FOLDER_PROPS.iter() + } else { + FILE_PROPS.iter() + }) + } else { + Box::new(fields.iter()) + }; + + // Fill properties + let mut fields = Vec::with_capacity(19); + let mut fields_not_found = Vec::new(); + for property in properties { + match property { + DavProperty::WebDav(dav_property) => match dav_property { + WebDavProperty::CreationDate => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::Timestamp(node.created.into()), + )); + } + WebDavProperty::DisplayName => { + if let Some(name) = node.display_name.as_ref() { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::String(name.to_string()), + )); + } else if !is_all_prop { + fields_not_found + .push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetContentLanguage => { + if !is_all_prop { + fields_not_found + .push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetContentLength => { + if let Some(value) = node.file.as_ref() { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::Uint64(u32::from(value.size) as u64), + )); + } else if !is_all_prop { + fields_not_found + .push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetContentType => { + if let Some(value) = + node.file.as_ref().and_then(|file| file.media_type.as_ref()) + { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::String(value.to_string()), + )); + } else if !is_all_prop { + fields_not_found + .push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::GetETag => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::String(node_.etag()), + )); + } + WebDavProperty::GetLastModified => { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::Rfc1123Date(Rfc1123DateTime::new( + node.modified.into(), + )), + )); + } + WebDavProperty::ResourceType => { + if node.file.is_none() { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ResourceType::Collection], + )); + } else { + fields.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::LockDiscovery => { + if let Some((path, lock)) = + locks.as_ref().and_then(|locks| locks.find_lock(&item.name)) + { + fields.push(DavPropertyValue::new( + property.clone(), + vec![lock.to_active_lock(query.format_to_base_uri(path))], + )); + } else { + fields.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::SupportedLock => { + fields.push(DavPropertyValue::new( + property.clone(), + SupportedLock::default(), + )); + } + WebDavProperty::SupportedReportSet => { + if node.file.is_none() { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ReportSet::SyncCollection], + )); + } else if !is_all_prop { + fields_not_found + .push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::SyncToken => { + fields.push(DavPropertyValue::new( + property.clone(), + sync_token.clone().unwrap(), + )); + } + WebDavProperty::CurrentUserPrincipal => todo!(), + WebDavProperty::QuotaAvailableBytes => todo!(), + WebDavProperty::QuotaUsedBytes => todo!(), + WebDavProperty::AlternateURISet => todo!(), + WebDavProperty::PrincipalURL => todo!(), + WebDavProperty::GroupMemberSet => todo!(), + WebDavProperty::GroupMembership => todo!(), + WebDavProperty::Owner => todo!(), + WebDavProperty::Group => { + if !is_all_prop { + fields_not_found + .push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::SupportedPrivilegeSet => todo!(), + WebDavProperty::CurrentUserPrivilegeSet => todo!(), + WebDavProperty::Acl => todo!(), + WebDavProperty::AclRestrictions => todo!(), + WebDavProperty::InheritedAclSet => todo!(), + WebDavProperty::PrincipalCollectionSet => todo!(), + }, + DavProperty::DeadProperty(tag) => { + if let Some(value) = node.dead_properties.find_tag(&tag.name) { + fields.push(DavPropertyValue::new(property.clone(), value)); + } else { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + property => { + if !is_all_prop { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + } + } + + // Add dead properties + if is_all_prop && !node.dead_properties.0.is_empty() { + node.dead_properties.to_dav_values(&mut fields); + } + + // Add response + let mut prop_stat = Vec::with_capacity(2); + if !fields.is_empty() { + prop_stat.push(PropStat::new_list(fields)); + } + if !fields_not_found.is_empty() { + prop_stat.push( + PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND), + ); + } + response.add_response(Response::new_propstat( + query.format_to_base_uri(&item.name), + prop_stat, + )); + + Ok(true) + }, + ) + .await + .caused_by(trc::location!())?; + 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), - ]; +static FOLDER_PROPS: [DavProperty; 19] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::Owner), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::Acl), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::InheritedAclSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), + DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes), + DavProperty::WebDav(WebDavProperty::QuotaUsedBytes), +]; - 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), - ]); - } +static FILE_PROPS: [DavProperty; 19] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::Owner), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::Acl), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::InheritedAclSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::GetContentLanguage), + DavProperty::WebDav(WebDavProperty::GetContentLength), + DavProperty::WebDav(WebDavProperty::GetContentType), +]; - props +struct Paths<'x> { + min: u32, + max: u32, + items: AHashMap, } -struct Paths<'x>(&'x AHashMap); +impl<'x> Paths<'x> { + pub fn new(iter: impl Iterator) -> Self { + let mut paths = Paths { + min: u32::MAX, + max: 0, + items: AHashMap::with_capacity(16), + }; + + for item in iter { + if item.document_id < paths.min { + paths.min = item.document_id; + } + + if item.document_id > paths.max { + paths.max = item.document_id; + } + + paths.items.insert(item.document_id, item); + } + paths + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } +} impl DocumentSet for Paths<'_> { fn min(&self) -> u32 { - unimplemented!() + self.min } fn max(&self) -> u32 { - unimplemented!() + self.max } fn contains(&self, id: u32) -> bool { - self.0.contains_key(&id) + self.items.contains_key(&id) } fn len(&self) -> usize { - self.0.len() + self.items.len() } fn iterate(&self) -> impl Iterator { - self.0.keys().copied() + self.items.keys().copied() } } diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index faf53af9..24bab344 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -103,8 +103,8 @@ impl FilePropPatchRequestHandler for Server { collection: resource.collection, document_id: resource.resource.into(), etag: node_.etag().into(), - lock_token: None, path: resource_.resource.unwrap(), + ..Default::default() }], Default::default(), DavMethod::PROPPATCH, diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index cce7e5e4..51ab50bb 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -101,8 +101,8 @@ impl FileUpdateRequestHandler for Server { collection: resource.collection, document_id: Some(document_id), etag: node_archive_.etag().into(), - lock_token: None, path: resource_name, + ..Default::default() }], Default::default(), DavMethod::PUT, @@ -218,9 +218,8 @@ impl FileUpdateRequestHandler for Server { account_id, collection: resource.collection, document_id: Some(u32::MAX), - etag: None, - lock_token: None, path: orig_resource_name, + ..Default::default() }], Default::default(), DavMethod::PUT, diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index ec98073e..b40c3f00 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -22,11 +22,13 @@ use hyper::{StatusCode, header}; use crate::{ DavError, DavMethod, DavResource, - common::{lock::LockRequestHandler, propfind::PropFindRequestHandler}, + common::{ + DavQuery, lock::LockRequestHandler, propfind::PropFindRequestHandler, uri::DavUriResource, + }, file::{ - acl::FileAclRequestHandler, changes::FileChangesRequestHandler, - copy_move::FileCopyMoveRequestHandler, delete::FileDeleteRequestHandler, - get::FileGetRequestHandler, mkcol::FileMkColRequestHandler, + acl::FileAclRequestHandler, copy_move::FileCopyMoveRequestHandler, + delete::FileDeleteRequestHandler, get::FileGetRequestHandler, + mkcol::FileMkColRequestHandler, propfind::HandleFilePropFindRequest, proppatch::FilePropPatchRequestHandler, update::FileUpdateRequestHandler, }, }; @@ -197,8 +199,24 @@ impl DavRequestDispatcher for Server { } DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? { Report::SyncCollection(sync_collection) => { - self.handle_file_changes_request(&access_token, headers, sync_collection) + let uri = self + .validate_uri(&access_token, headers.uri) .await + .and_then(|d| d.into_owned_uri())?; + match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::File => { + self.handle_file_propfind_request( + &access_token, + DavQuery::changes(uri, sync_collection, headers), + ) + .await + } + DavResource::Principal => { + Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)) + } + } } Report::Addressbook(addressbook_query) => todo!(), Report::AdressbookMultiGet(multi_get) => todo!(), diff --git a/crates/email/src/mailbox/destroy.rs b/crates/email/src/mailbox/destroy.rs index 06211971..b2c3ed11 100644 --- a/crates/email/src/mailbox/destroy.rs +++ b/crates/email/src/mailbox/destroy.rs @@ -100,59 +100,64 @@ impl MailboxDestroy for Server { // If the message is in multiple mailboxes, untag it from the current mailbox, // otherwise delete it. let mut destroy_ids = RoaringBitmap::new(); - for (message_id, message_data_) in self - .get_properties::, _>( - account_id, - Collection::Email, - &message_ids, - Property::Value, - ) - .await? - { - // Remove mailbox from list - let prev_message_data = message_data_ - .to_unarchived::() - .caused_by(trc::location!())?; + let mut batch = BatchBuilder::new(); - if !prev_message_data - .inner - .mailboxes - .iter() - .any(|id| id.mailbox_id == document_id) - { - continue; - } + self.get_archives( + account_id, + Collection::Email, + &message_ids, + Property::Value, + |message_id, message_data_| { + // Remove mailbox from list + let prev_message_data = message_data_ + .to_unarchived::() + .caused_by(trc::location!())?; + if !prev_message_data + .inner + .mailboxes + .iter() + .any(|id| id.mailbox_id == document_id) + { + return Ok(true); + } - if prev_message_data.inner.mailboxes.len() == 1 { - // Delete message - destroy_ids.insert(message_id); - continue; - } + if prev_message_data.inner.mailboxes.len() == 1 { + // Delete message + destroy_ids.insert(message_id); + return Ok(true); + } - let mut new_message_data = prev_message_data - .deserialize() - .caused_by(trc::location!())?; - let thread_id = new_message_data.thread_id; + let mut new_message_data = prev_message_data + .deserialize() + .caused_by(trc::location!())?; + let thread_id = new_message_data.thread_id; - new_message_data - .mailboxes - .retain(|id| id.mailbox_id != document_id); + new_message_data + .mailboxes + .retain(|id| id.mailbox_id != document_id); - // Untag message from mailbox - let mut batch = BatchBuilder::new(); - batch - .with_account_id(account_id) - .with_collection(Collection::Email) - .update_document(message_id) - .custom( - ObjectIndexBuilder::new() - .with_changes(new_message_data) - .with_current(prev_message_data), - ) - .caused_by(trc::location!())?; + // Untag message from mailbox + batch + .with_account_id(account_id) + .with_collection(Collection::Email) + .update_document(message_id) + .custom( + ObjectIndexBuilder::new() + .with_changes(new_message_data) + .with_current(prev_message_data), + ) + .caused_by(trc::location!())?; + changes + .log_update(Collection::Email, Id::from_parts(thread_id, message_id)); + Ok(true) + }, + ) + .await + .caused_by(trc::location!())?; + + if !batch.is_empty() { match self.core.storage.data.write(batch.build()).await { - Ok(_) => changes - .log_update(Collection::Email, Id::from_parts(thread_id, message_id)), + Ok(_) => {} Err(err) if err.is_assertion_failure() => { return Ok(Err(SetError::forbidden().with_description(concat!( "Another process modified a message in this mailbox ", diff --git a/crates/email/src/message/delete.rs b/crates/email/src/message/delete.rs index 364e6dd3..e1fcf166 100644 --- a/crates/email/src/message/delete.rs +++ b/crates/email/src/message/delete.rs @@ -58,7 +58,7 @@ impl EmailDeletion for Server { async fn emails_tombstone( &self, account_id: u32, - mut document_ids: RoaringBitmap, + document_ids: RoaringBitmap, ) -> trc::Result<(ChangeLogBuilder, RoaringBitmap)> { // Create batch let mut changes = ChangeLogBuilder::with_change_id(0); @@ -68,69 +68,73 @@ impl EmailDeletion for Server { batch .with_account_id(account_id) .with_collection(Collection::Email); + let mut batches = Vec::new(); + let mut deleted_ids = RoaringBitmap::new(); + self.get_archives( + account_id, + Collection::Email, + &document_ids, + Property::Value, + |document_id, data_| { + let data = data_ + .to_unarchived::() + .caused_by(trc::location!())?; + let thread_id = u32::from(data.inner.thread_id); - for (document_id, data_) in self - .get_properties::, _>( - account_id, - Collection::Email, - &document_ids, - Property::Value, - ) - .await? - { - let data = data_ - .to_unarchived::() - .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)); + } - // Log mailbox changes - for mailbox in data.inner.mailboxes.iter() { - changes.log_child_update(Collection::Mailbox, u32::from(mailbox.mailbox_id)); - } + // Log message deletion + changes.log_delete(Collection::Email, Id::from_parts(thread_id, document_id)); - // Log message deletion - changes.log_delete(Collection::Email, Id::from_parts(thread_id, document_id)); + // Log thread changes + changes.log_child_update(Collection::Thread, thread_id); - // Log thread changes - changes.log_child_update(Collection::Thread, thread_id); + // Add changes to batch + batch + .update_document(document_id) + .custom(ObjectIndexBuilder::<_, ()>::new().with_current(data)) + .caused_by(trc::location!())? + .tag( + Property::MailboxIds, + TagValue::Id(MaybeDynamicId::Static(TOMBSTONE_ID)), + ); - // Add changes to batch - batch - .update_document(document_id) - .custom(ObjectIndexBuilder::<_, ()>::new().with_current(data)) - .caused_by(trc::location!())? - .tag( - Property::MailboxIds, - TagValue::Id(MaybeDynamicId::Static(TOMBSTONE_ID)), - ); + deleted_ids.insert(document_id); - document_ids.remove(document_id); + if batch.ops.len() >= 1000 { + batches.push(std::mem::replace(&mut batch, BatchBuilder::new())); + batch + .with_account_id(account_id) + .with_collection(Collection::Email); + } - if batch.ops.len() >= 1000 { + Ok(true) + }, + ) + .await?; + + for batch in batches.into_iter().chain([batch]) { + if !batch.is_empty() { self.core .storage .data .write(batch.build()) .await .caused_by(trc::location!())?; - - batch = BatchBuilder::new(); - batch - .with_account_id(account_id) - .with_collection(Collection::Email); } } - if !batch.ops.is_empty() { - self.core - .storage - .data - .write(batch.build()) - .await - .caused_by(trc::location!())?; - } + let not_destroyed = if document_ids.len() == deleted_ids.len() { + RoaringBitmap::new() + } else { + deleted_ids ^= document_ids; + deleted_ids + }; - Ok((changes, document_ids)) + Ok((changes, not_destroyed)) } async fn purge_accounts(&self) { @@ -240,19 +244,20 @@ impl EmailDeletion for Server { // Find messages to destroy let mut destroy_ids = RoaringBitmap::new(); - for (document_id, data) in self - .get_properties::, _>( - account_id, - Collection::Email, - &deletion_candidates, - Property::Value, - ) - .await? - { - if data.unarchive::()?.change_id < reference_cid { - destroy_ids.insert(document_id); - } - } + self.get_archives( + account_id, + Collection::Email, + &deletion_candidates, + Property::Value, + |document_id, data| { + if data.unarchive::()?.change_id < reference_cid { + destroy_ids.insert(document_id); + } + + Ok(true) + }, + ) + .await?; if destroy_ids.is_empty() { return Ok(()); diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 582c27a5..dad9598d 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -159,39 +159,40 @@ impl SessionData { let mut special_uses = AHashMap::new(); let mut mailbox_topology = TopologicalSort::with_capacity(10); - for (mailbox_id, mailbox_) in self - .server - .get_properties::, _>( + self.server + .get_archives( account_id, Collection::Mailbox, &mailbox_ids, Property::Value, + |mailbox_id, mailbox_| { + let mailbox = mailbox_ + .unarchive::() + .caused_by(trc::location!())?; + // Map special uses + let role = SpecialUse::from(&mailbox.role); + if !matches!(mailbox.role, ArchivedSpecialUse::None) { + special_uses.insert(role, mailbox_id); + } + + // Build mailbox data + let mailbox = MailboxData { + mailbox_id, + parent_id: u32::from(mailbox.parent_id), + role, + name: mailbox.name.to_string(), + is_subscribed: mailbox.is_subscribed(access_token.primary_id()), + }; + mailbox_topology.insert(mailbox.parent_id, mailbox.mailbox_id + 1); + + // Add mailbox id + mailboxes.insert(mailbox.mailbox_id, mailbox); + + Ok(true) + }, ) .await - .caused_by(trc::location!())? - { - let mailbox = mailbox_ - .unarchive::() - .caused_by(trc::location!())?; - // Map special uses - let role = SpecialUse::from(&mailbox.role); - if !matches!(mailbox.role, ArchivedSpecialUse::None) { - special_uses.insert(role, mailbox_id); - } - - // Build mailbox data - let mailbox = MailboxData { - mailbox_id, - parent_id: u32::from(mailbox.parent_id), - role, - name: mailbox.name.to_string(), - is_subscribed: mailbox.is_subscribed(access_token.primary_id()), - }; - mailbox_topology.insert(mailbox.parent_id, mailbox.mailbox_id + 1); - - // Add mailbox id - mailboxes.insert(mailbox.mailbox_id, mailbox); - } + .caused_by(trc::location!())?; // Build account let message_ids = self diff --git a/crates/imap/src/core/message.rs b/crates/imap/src/core/message.rs index 0a94ee48..b06324a6 100644 --- a/crates/imap/src/core/message.rs +++ b/crates/imap/src/core/message.rs @@ -50,40 +50,40 @@ impl SessionData { // Obtain all message ids let mut uid_map = BTreeMap::new(); - for (message_id, message_data_) in self - .server - .get_properties::, _>( + self.server + .get_archives( mailbox.account_id, Collection::Email, &message_ids, Property::Value, + |message_id, message_data_| { + let message_data = message_data_ + .unarchive::() + .caused_by(trc::location!())?; + // Make sure the message is still in this mailbox + if let Some(item) = message_data + .mailboxes + .iter() + .find(|item| item.mailbox_id == mailbox.mailbox_id) + { + debug_assert!(item.uid != 0, "UID is zero for message {item:?}"); + if uid_map.insert(u32::from(item.uid), message_id).is_some() { + trc::event!( + Store(trc::StoreEvent::UnexpectedError), + AccountId = mailbox.account_id, + Collection = Collection::Mailbox, + MailboxId = mailbox.mailbox_id, + MessageId = message_id, + SpanId = self.session_id, + Details = "Duplicate IMAP UID" + ); + } + } + + Ok(true) + }, ) - .await? - .into_iter() - { - let message_data = message_data_ - .unarchive::() - .caused_by(trc::location!())?; - // Make sure the message is still in this mailbox - if let Some(item) = message_data - .mailboxes - .iter() - .find(|item| item.mailbox_id == mailbox.mailbox_id) - { - debug_assert!(item.uid != 0, "UID is zero for message {item:?}"); - if uid_map.insert(u32::from(item.uid), message_id).is_some() { - trc::event!( - Store(trc::StoreEvent::UnexpectedError), - AccountId = mailbox.account_id, - Collection = Collection::Mailbox, - MailboxId = mailbox.mailbox_id, - MessageId = message_id, - SpanId = self.session_id, - Details = "Duplicate IMAP UID" - ); - } - } - } + .await?; // Obtain UID next and assign UIDs let mut uid_max = 0; diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs index d8d7a60f..562f0f7d 100644 --- a/crates/imap/src/op/expunge.rs +++ b/crates/imap/src/op/expunge.rs @@ -24,7 +24,7 @@ use jmap_proto::types::{ }; use store::{ roaring::RoaringBitmap, - write::{AlignedBytes, Archive, BatchBuilder, log::ChangeLogBuilder}, + write::{BatchBuilder, log::ChangeLogBuilder}, }; use super::{ImapContext, ToModSeq}; @@ -189,53 +189,62 @@ impl SessionData { changelog: &mut ChangeLogBuilder, ) -> trc::Result<()> { let mut destroy_ids = RoaringBitmap::new(); + let mut batch = BatchBuilder::new(); + batch + .with_account_id(account_id) + .with_collection(Collection::Email); - for (id, data_) in self - .server - .get_properties::, _>( + self.server + .get_archives( account_id, Collection::Email, deleted_ids, Property::Value, + |id, data_| { + let data = data_ + .to_unarchived::() + .caused_by(trc::location!())?; + + if !data.inner.has_mailbox_id(mailbox_id) { + return Ok(true); + } else if data.inner.mailboxes.len() == 1 { + destroy_ids.insert(id); + return Ok(true); + } + + // Prepare changes + let mut new_data = data.deserialize().caused_by(trc::location!())?; + if changelog.change_id == u64::MAX { + changelog.change_id = self.server.assign_change_id(account_id)? + } + + new_data.change_id = changelog.change_id; + let thread_id = new_data.thread_id; + + // Untag message from this mailbox and remove Deleted flag + new_data.remove_mailbox(mailbox_id); + new_data.remove_keyword(&Keyword::Deleted); + + changelog.log_update(Collection::Email, Id::from_parts(thread_id, id)); + changelog.log_child_update(Collection::Mailbox, mailbox_id); + + // Write changes + batch + .update_document(id) + .custom( + ObjectIndexBuilder::new() + .with_current(data) + .with_changes(new_data), + ) + .caused_by(trc::location!())?; + + Ok(true) + }, ) .await - .caused_by(trc::location!())? - { - let data = data_ - .to_unarchived::() - .caused_by(trc::location!())?; + .caused_by(trc::location!())?; - if !data.inner.has_mailbox_id(mailbox_id) { - continue; - } else if data.inner.mailboxes.len() == 1 { - destroy_ids.insert(id); - continue; - } - - // Prepare changes - let mut new_data = data.deserialize().caused_by(trc::location!())?; - if changelog.change_id == u64::MAX { - changelog.change_id = self.server.assign_change_id(account_id)? - } - new_data.change_id = changelog.change_id; - let thread_id = new_data.thread_id; - - // Untag message from this mailbox and remove Deleted flag - new_data.remove_mailbox(mailbox_id); - new_data.remove_keyword(&Keyword::Deleted); - - // Write changes - let mut batch = BatchBuilder::new(); - batch - .with_account_id(account_id) - .with_collection(Collection::Email) - .update_document(id) - .custom( - ObjectIndexBuilder::new() - .with_current(data) - .with_changes(new_data), - ) - .caused_by(trc::location!())?; + if !batch.is_empty() { match self .server .store() @@ -243,10 +252,7 @@ impl SessionData { .await .caused_by(trc::location!()) { - Ok(_) => { - changelog.log_update(Collection::Email, Id::from_parts(thread_id, id)); - changelog.log_child_update(Collection::Mailbox, mailbox_id); - } + Ok(_) => {} Err(err) => { if !err.is_assertion_failure() { return Err(err.caused_by(trc::location!())); diff --git a/crates/pop3/src/mailbox.rs b/crates/pop3/src/mailbox.rs index 2e126f11..447adeb4 100644 --- a/crates/pop3/src/mailbox.rs +++ b/crates/pop3/src/mailbox.rs @@ -125,30 +125,29 @@ impl Session { .caused_by(trc::location!())?; // Sort by UID - for (message_id, uid_mailbox) in self - .server - .get_properties::, _>( + self.server + .get_archives( account_id, Collection::Email, &message_ids, Property::Value, + |message_id, uid_mailbox| { + // Make sure the message is still in Inbox + if let Some(item) = uid_mailbox + .unarchive::() + .caused_by(trc::location!())? + .mailboxes + .iter() + .find(|item| item.mailbox_id == INBOX_ID) + { + debug_assert!(item.uid != 0, "UID is zero for message {item:?}"); + message_map.insert(u32::from(item.uid), message_id); + } + Ok(true) + }, ) .await - .caused_by(trc::location!())? - .into_iter() - { - // Make sure the message is still in Inbox - if let Some(item) = uid_mailbox - .unarchive::() - .caused_by(trc::location!())? - .mailboxes - .iter() - .find(|item| item.mailbox_id == INBOX_ID) - { - debug_assert!(item.uid != 0, "UID is zero for message {item:?}"); - message_map.insert(u32::from(item.uid), message_id); - } - } + .caused_by(trc::location!())?; // Create mailbox let mut mailbox = Mailbox {