Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::RequestHeaders;
use groupware::{
DestroyArchive,
cache::GroupwareCache,
contact::{AddressBook, ContactCard},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::{BatchBuilder, ValueClass};
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
field::PrincipalField,
};
pub(crate) trait CardDeleteRequestHandler: Sync + Send {
fn handle_card_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardDeleteRequestHandler for Server {
async fn handle_card_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> crate::Result<HttpResponse> {
// Validate URI
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())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
// Check resource type
let delete_resource = resources
.by_path(delete_path)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let document_id = delete_resource.document_id();
// Fetch entry
let mut batch = BatchBuilder::new();
if delete_resource.is_container() {
let book_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::AddressBook,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let book = book_
.to_unarchived::<AddressBook>()
.caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !book
.inner
.acls
.effective_acl(access_token)
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::AddressBook,
document_id: document_id.into(),
etag: book.etag().into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
// Delete addressbook and cards
DestroyArchive(book)
.delete_with_cards(
self,
access_token.account_tenant_ids(),
account_id,
document_id,
resources
.subtree(delete_path)
.filter(|r| !r.is_container())
.map(|r| r.document_id())
.collect::<Vec<_>>(),
resources.format_resource(delete_resource).into(),
&mut batch,
)
.await
.caused_by(trc::location!())?;
// Reset default address book id
let default_book_id = self
.store()
.get_value::<u32>(ValueKey {
account_id,
collection: Collection::Principal.into(),
document_id: 0,
class: ValueClass::Property(PrincipalField::DefaultAddressBookId.into()),
})
.await
.caused_by(trc::location!())?;
if default_book_id.is_some_and(|id| id == document_id) {
batch
.with_account_id(account_id)
.with_collection(Collection::Principal)
.with_document(0)
.clear(PrincipalField::DefaultAddressBookId);
}
} else {
// Validate ACL
let addressbook_id = delete_resource.parent_id().unwrap();
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
addressbook_id,
Acl::RemoveItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let card_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::ContactCard,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::ContactCard,
document_id: document_id.into(),
etag: card_.etag().into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
// Delete card
DestroyArchive(
card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?,
)
.delete(
access_token.account_tenant_ids(),
account_id,
document_id,
addressbook_id,
resources.format_resource(delete_resource).into(),
&mut batch,
)
.caused_by(trc::location!())?;
}
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
}
+134
View File
@@ -0,0 +1,134 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
use groupware::{cache::GroupwareCache, contact::ContactCard};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CardGetRequestHandler: Sync + Send {
fn handle_card_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardGetRequestHandler for Server {
async fn handle_card_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let resource = resources
.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
resource.parent_id().unwrap(),
Acl::ReadItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Fetch card
let card_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::ContactCard,
resource.document_id(),
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let card = card_
.unarchive::<ContactCard>()
.caused_by(trc::location!())?;
// Validate headers
let etag = card_.etag();
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::ContactCard,
document_id: resource.document_id().into(),
etag: etag.clone().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::GET,
)
.await?;
let response = HttpResponse::new(StatusCode::OK)
.with_content_type("text/vcard; charset=utf-8")
.with_etag(etag)
.with_last_modified(Rfc1123DateTime::new(i64::from(card.modified)).to_string());
let mut vcard = String::with_capacity(128);
let _ = card.card.write_to(
&mut vcard,
headers
.vcard_version
.unwrap_or(self.core.groupware.vcard_version),
);
if !is_head {
Ok(response.with_binary_body(vcard))
} else {
Ok(response.with_content_length(vcard.len()))
}
}
}
+151
View File
@@ -0,0 +1,151 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::proppatch::CardPropPatchRequestHandler;
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{Namespace, request::MkCol, response::MkColResponse},
};
use groupware::{
cache::GroupwareCache,
contact::{AddressBook, AddressBookPreferences},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use trc::AddContext;
use types::collection::{Collection, SyncCollection};
pub(crate) trait CardMkColRequestHandler: Sync + Send {
fn handle_card_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardMkColRequestHandler for Server {
async fn handle_card_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let name = resource
.resource
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
if !access_token.is_member(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
} else if name.contains('/')
|| self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?
.by_path(name)
.is_some()
{
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: name,
..Default::default()
}],
Default::default(),
DavMethod::MKCOL,
)
.await?;
// Build file container
let mut book = AddressBook {
name: name.to_string(),
preferences: vec![AddressBookPreferences {
account_id,
name: "Address Book".to_string(),
..Default::default()
}],
..Default::default()
};
// Apply MKCOL properties
let mut return_prop_stat = None;
if let Some(mkcol) = request {
let mut prop_stat = PropStatBuilder::default();
if !self.apply_addressbook_properties(
access_token.personal_id(account_id, Collection::AddressBook),
&mut book,
false,
mkcol.props,
&mut prop_stat,
) {
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::CardDav)
.to_string(),
));
}
if headers.ret != Return::Minimal {
return_prop_stat = Some(prop_stat);
}
}
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::AddressBook, 1)
.await
.caused_by(trc::location!())?;
book.insert(
access_token.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
if let Some(prop_stat) = return_prop_stat {
Ok(HttpResponse::new(StatusCode::CREATED)
.with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::CardDav)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
}
}
+107
View File
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{DavError, DavErrorCondition};
use common::{DavResources, Server};
use dav_proto::schema::{
property::{CardDavProperty, DavProperty, WebDavProperty},
response::CardCondition,
};
use hyper::StatusCode;
use trc::AddContext;
use types::{collection::Collection, field::ContactField};
pub mod copy_move;
pub mod delete;
pub mod get;
pub mod mkcol;
pub mod proppatch;
pub mod query;
pub mod update;
pub(crate) static CARD_CONTAINER_PROPS: [DavProperty; 23] = [
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),
DavProperty::CardDav(CardDavProperty::AddressbookDescription),
DavProperty::CardDav(CardDavProperty::SupportedAddressData),
DavProperty::CardDav(CardDavProperty::SupportedCollationSet),
DavProperty::CardDav(CardDavProperty::MaxResourceSize),
];
pub(crate) static CARD_ITEM_PROPS: [DavProperty; 20] = [
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),
DavProperty::CardDav(CardDavProperty::AddressData {
properties: Vec::new(),
version: None,
}),
];
pub(crate) async fn assert_is_unique_uid(
server: &Server,
resources: &DavResources,
account_id: u32,
addressbook_id: u32,
uid: Option<&str>,
) -> crate::Result<()> {
if let Some(uid) = uid {
let hits = server
.document_ids_matching(
account_id,
Collection::ContactCard,
ContactField::Uid,
uid.as_bytes(),
)
.await
.caused_by(trc::location!())?;
if !hits.is_empty() {
for path in resources.children(addressbook_id) {
if hits.contains(path.document_id()) {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(resources.format_resource(path).into()),
)));
}
}
}
}
Ok(())
}
+488
View File
@@ -0,0 +1,488 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{
Namespace,
property::{CardDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty},
request::{DavPropertyValue, PropertyUpdate},
response::{BaseCondition, MultiStatus, Response},
},
};
use groupware::{
cache::GroupwareCache,
contact::{AddressBook, ContactCard},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CardPropPatchRequestHandler: Sync + Send {
fn handle_card_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: PropertyUpdate,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn apply_addressbook_properties(
&self,
personal_id: u32,
address_book: &mut AddressBook,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool;
fn apply_card_properties(
&self,
card: &mut ContactCard,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool;
}
impl CardPropPatchRequestHandler for Server {
async fn handle_card_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut request: PropertyUpdate,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let uri = headers.uri;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let resource = resource_
.resource
.and_then(|r| resources.by_path(r))
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let document_id = resource.document_id();
let collection = if resource.is_container() {
Collection::AddressBook
} else {
Collection::ContactCard
};
if !request.has_changes() {
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
// Verify ACL
if !access_token.is_member(account_id) {
let (acl, document_id) = if resource.is_container() {
(Acl::Modify, resource.document_id())
} else {
(Acl::ModifyItems, resource.parent_id().unwrap())
};
if !resources.has_access_to_container(access_token, document_id, acl) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
}
// Fetch archive
let archive = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
collection,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection,
document_id: document_id.into(),
etag: archive.etag().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::PROPPATCH,
)
.await?;
let is_success;
let mut batch = BatchBuilder::new();
let mut items = PropStatBuilder::default();
let etag = if resource.is_container() {
// Deserialize
let book = archive
.to_unarchived::<AddressBook>()
.caused_by(trc::location!())?;
let mut new_book = archive
.deserialize::<AddressBook>()
.caused_by(trc::location!())?;
let personal_id = access_token.personal_id(account_id, Collection::AddressBook);
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_addressbook_properties(
personal_id,
&mut new_book,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
is_success = self.apply_addressbook_properties(
personal_id,
&mut new_book,
true,
request.set,
&mut items,
);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_addressbook_properties(
personal_id,
&mut new_book,
request.remove,
&mut items,
);
}
if is_success {
new_book
.update(
access_token.account_tenant_ids(),
book,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
book.etag().into()
}
} else {
// Deserialize
let card = archive
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?;
let mut new_card = archive
.deserialize::<ContactCard>()
.caused_by(trc::location!())?;
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_card_properties(
&mut new_card,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
is_success = self.apply_card_properties(&mut new_card, true, request.set, &mut items);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_card_properties(&mut new_card, request.remove, &mut items);
}
if is_success {
new_card
.update(
access_token.account_tenant_ids(),
card,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
card.etag().into()
}
};
if is_success {
self.commit_batch(batch).await.caused_by(trc::location!())?;
}
if headers.ret != Return::Minimal || !is_success {
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(
MultiStatus::new(vec![Response::new_propstat(uri, items.build())])
.with_namespace(Namespace::CardDav)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
}
fn apply_addressbook_properties(
&self,
personal_id: u32,
address_book: &mut AddressBook,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool {
let mut has_errors = false;
for property in properties {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
address_book.preferences_mut(personal_id).name = name;
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(
DavProperty::CardDav(CardDavProperty::AddressbookDescription),
DavValue::String(name),
) => {
if name.len() <= self.core.groupware.live_property_size {
address_book.preferences_mut(personal_id).description = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
address_book.created = dt;
items.insert_ok(property.property);
}
(
DavProperty::WebDav(WebDavProperty::ResourceType),
DavValue::ResourceTypes(types),
) => {
if !types.0.iter().all(|rt| {
matches!(rt, ResourceType::Collection | ResourceType::AddressBook)
}) {
items.insert_precondition_failed(
property.property,
StatusCode::FORBIDDEN,
BaseCondition::ValidResourceType,
);
has_errors = true;
} else {
items.insert_ok(property.property);
}
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
if is_update {
address_book.dead_properties.remove_element(dead);
}
if address_book.dead_properties.size() + values.size() + dead.size()
< self.core.groupware.dead_property_size.unwrap()
{
address_book
.dead_properties
.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
fn apply_card_properties(
&self,
card: &mut ContactCard,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool {
let mut has_errors = false;
for property in properties {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
card.display_name = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
card.created = dt;
items.insert_ok(property.property);
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
if is_update {
card.dead_properties.remove_element(dead);
}
if card.dead_properties.size() + values.size() + dead.size()
< self.core.groupware.dead_property_size.unwrap()
{
card.dead_properties.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
}
fn remove_card_properties(
card: &mut ContactCard,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::WebDav(WebDavProperty::DisplayName) => {
card.display_name = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
card.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
fn remove_addressbook_properties(
personal_id: u32,
book: &mut AddressBook,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::CardDav(CardDavProperty::AddressbookDescription) => {
book.preferences_mut(personal_id).description = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::WebDav(WebDavProperty::DisplayName) => {
book.preferences_mut(personal_id).name.clear();
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
book.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
+235
View File
@@ -0,0 +1,235 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError,
common::{
AddressbookFilter, DavQuery,
propfind::{PropFindItem, PropFindRequestHandler},
uri::DavUriResource,
},
};
use calcard::vcard::{
ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName, VCardProperty,
VCardVersion,
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders,
schema::{
property::CardDavPropertyName,
request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup},
response::MultiStatus,
},
};
use groupware::cache::GroupwareCache;
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::fmt::Write;
use trc::AddContext;
use types::{acl::Acl, collection::SyncCollection};
pub(crate) trait CardQueryRequestHandler: Sync + Send {
fn handle_card_query_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: AddressbookQuery,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardQueryRequestHandler for Server {
async fn handle_card_query_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: AddressbookQuery,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let Some(resource) = resources.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
) else {
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(MultiStatus::not_found(headers.uri).to_string()));
};
if !resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
resources
.shared_items(access_token, [Acl::ReadItems], false)
.into()
} else {
None
};
// Obtain document ids in folder
let mut items = Vec::with_capacity(16);
for resource in resources.children(resource.document_id()) {
if shared_ids
.as_ref()
.is_none_or(|ids| ids.contains(resource.document_id()))
{
items.push(PropFindItem::new(
resources.format_resource(resource),
account_id,
resource,
));
}
}
self.handle_dav_query(
access_token,
DavQuery::addressbook_query(request, items, headers),
)
.await
}
}
pub(crate) fn vcard_query(card: &ArchivedVCard, filters: &AddressbookFilter) -> bool {
let mut is_all = true;
let mut matches_one = false;
for filter in filters {
match filter {
Filter::AnyOf => {
is_all = false;
}
Filter::AllOf => {
is_all = true;
}
Filter::Property { prop, op, .. } => {
let mut properties = find_properties(card, prop).peekable();
let result = if properties.peek().is_some() {
properties.any(|entry| match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
let mut matched_any = false;
for value in entry.values.iter() {
if let Some(text) = value.as_text()
&& text_match.matches(text)
{
matched_any = true;
break;
}
}
matched_any
}
FilterOp::TimeRange(_) => false,
})
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Parameter {
prop, param, op, ..
} => {
let mut properties = find_properties(card, prop)
.filter_map(|entry| find_parameter(entry, param))
.peekable();
let result = if properties.peek().is_some() {
properties.any(|entry| match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
if let Some(text) = entry.value.as_text() {
text_match.matches(text)
} else {
false
}
}
FilterOp::TimeRange(_) => false,
})
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Component { .. } => {}
}
}
is_all || matches_one
}
#[inline(always)]
fn find_properties<'x>(
card: &'x ArchivedVCard,
prop: &VCardPropertyWithGroup,
) -> impl Iterator<Item = &'x ArchivedVCardEntry> {
card.entries
.iter()
.filter(move |entry| entry.name == prop.name && entry.group == prop.group)
}
#[inline(always)]
fn find_parameter<'x>(
entry: &'x ArchivedVCardEntry,
name: &VCardParameterName,
) -> Option<&'x ArchivedVCardParameter> {
entry.params.iter().find(|param| param.name == *name)
}
pub(crate) fn serialize_vcard_with_props(
card: &ArchivedVCard,
props: &[CardDavPropertyName],
version: VCardVersion,
) -> String {
let mut vcard = String::with_capacity(128);
if !props.is_empty() {
let _ = write!(&mut vcard, "BEGIN:VCARD\r\n");
let is_v4 = matches!(version, VCardVersion::V4_0);
for entry in card.entries.iter() {
for item in props {
if entry.name == item.name && entry.group == item.group {
if item.name != VCardProperty::Version {
let _ = entry.write_to(&mut vcard, !item.no_value, is_v4);
} else {
let _ = write!(&mut vcard, "VERSION:{version}\r\n");
}
break;
}
}
}
let _ = write!(&mut vcard, "END:VCARD\r\n");
} else {
let _ = card.write_to(&mut vcard, version);
}
vcard
}
+305
View File
@@ -0,0 +1,305 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::assert_is_unique_uid;
use crate::{
DavError, DavErrorCondition, DavMethod,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
fix_percent_encoding,
};
use calcard::{Entry, Parser};
use common::{DavName, Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{property::Rfc1123DateTime, response::CardCondition},
};
use groupware::{cache::GroupwareCache, contact::ContactCard};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CardUpdateRequestHandler: Sync + Send {
fn handle_card_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardUpdateRequestHandler for Server {
async fn handle_card_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let resource_name = fix_percent_encoding(
resource
.resource
.ok_or(DavError::Code(StatusCode::CONFLICT))?,
);
if bytes.len() > self.core.groupware.max_vcard_size {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::MaxResourceSize(self.core.groupware.max_vcard_size as u32),
)));
}
let vcard_raw = std::str::from_utf8(&bytes).map_err(|_| {
DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::SupportedAddressData,
)
.with_details("The request body is not valid UTF-8."),
)
})?;
let vcard = match Parser::new(vcard_raw).strict().entry() {
Entry::VCard(vcard) => vcard,
_ => {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::SupportedAddressData,
)
.with_details("Failed to parse vCard data."),
));
}
};
if let Some(resource) = resources.by_path(resource_name.as_ref()) {
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
let parent_id = resource.parent_id().unwrap();
let document_id = resource.document_id();
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Update
let card_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::ContactCard,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let card = card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?;
// Validate headers
match self
.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::ContactCard,
document_id: Some(document_id),
etag: card.etag().into(),
path: resource_name.as_ref(),
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await
{
Ok(_) => {}
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
if headers.ret == Return::Representation =>
{
let mut vcard = String::with_capacity(128);
let _ = card.inner.card.write_to(
&mut vcard,
headers
.vcard_version
.unwrap_or(self.core.groupware.vcard_version),
);
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
.with_content_type("text/vcard; charset=utf-8")
.with_etag(card.etag())
.with_last_modified(
Rfc1123DateTime::new(i64::from(card.inner.modified)).to_string(),
)
.with_header("Preference-Applied", "return=representation")
.with_binary_body(vcard));
}
Err(e) => return Err(e),
}
// Validate UID
match (card.inner.card.uid(), vcard.uid()) {
(Some(old_uid), Some(new_uid)) if old_uid == new_uid => {}
(None, None) | (None, Some(_)) => {}
_ => {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(resources.format_resource(resource).into()),
)));
}
}
// Validate quota
let extra_bytes =
(bytes.len() as u64).saturating_sub(u32::from(card.inner.size) as u64);
if extra_bytes > 0 {
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
.await?;
}
// Build node
let mut new_card = card
.deserialize::<ContactCard>()
.caused_by(trc::location!())?;
new_card.size = bytes.len() as u32;
new_card.card = vcard;
// Prepare write batch
let mut batch = BatchBuilder::new();
let etag = new_card
.update(
access_token.account_tenant_ids(),
card,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
} else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) {
if !parent.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
parent.document_id(),
Acl::AddItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: resource_name.as_ref(),
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await?;
// Validate UID
assert_is_unique_uid(
self,
&resources,
account_id,
parent.document_id(),
vcard.uid(),
)
.await?;
// Validate quota
if !bytes.is_empty() {
self.has_available_quota(
self.account(account_id).await?.as_ref(),
bytes.len() as u64,
)
.await?;
}
// Build node
let card = ContactCard {
names: vec![DavName {
name: name.to_string(),
parent_id: parent.document_id(),
}],
card: vcard,
size: bytes.len() as u32,
..Default::default()
};
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::ContactCard, 1)
.await
.caused_by(trc::location!())?;
let etag = card
.insert(
access_token.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
} else {
Err(DavError::Code(StatusCode::CONFLICT))?
}
}
}