From dd95efe3cef08e375af7b8383ca9e846ab60a7bf Mon Sep 17 00:00:00 2001
From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com>
Date: Sat, 18 Apr 2026 10:42:21 +0200
Subject: [PATCH] WebDAV and other minor fixes
---
CHANGELOG.md | 5 +++-
crates/common/src/network/mta.rs | 14 +++++++++++
crates/dav-proto/src/responses/property.rs | 2 +-
crates/dav/src/calendar/freebusy.rs | 2 +-
crates/dav/src/calendar/query.rs | 2 +-
crates/dav/src/card/query.rs | 2 +-
crates/http/src/request.rs | 4 +++-
tests/src/webdav/acl.rs | 28 ++++++++++++++++++++++
8 files changed, 53 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e55e0118..21b6e85a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -86,7 +86,10 @@ This version includes **multiple breaking changes**. If you are upgrading from v
- Increment argument max length to `8000` bytes
- ACL: Add `RIGHTS` capability (#2762)
- ACL: Fix `ACL SET` permission override.
-- WebDAV: Return `304` `NOT_MODIFIED` on `If-None-Match`
+- WebDAV:
+ - Return `304` `NOT_MODIFIED` on `If-None-Match`
+ - Use RFC 2616 instead of RFC 1123 for date formatting
+ - Fix ACL container/item mismatch in reports.
- Configuration: Prefix parsing issues (#2495)
- OIDC: JWKS Exposes Symmetric Signing Key
- SQLite: Fix thread pool exhaustion.
diff --git a/crates/common/src/network/mta.rs b/crates/common/src/network/mta.rs
index d7de05f4..f0ed036e 100644
--- a/crates/common/src/network/mta.rs
+++ b/crates/common/src/network/mta.rs
@@ -375,6 +375,20 @@ impl Server {
})
.caused_by(trc::location!())?
{
+ let last_trained_at = match &model {
+ SpamClassifier::FhClassifier {
+ last_trained_at, ..
+ } => Some(*last_trained_at),
+ SpamClassifier::CcfhClassifier {
+ last_trained_at, ..
+ } => Some(*last_trained_at),
+ SpamClassifier::Disabled => None,
+ };
+
+ trc::event!(
+ Spam(SpamEvent::ModelLoaded),
+ Details = last_trained_at.map(trc::Value::Timestamp),
+ );
self.inner.data.spam_classifier.store(Arc::new(model));
} else {
trc::event!(Spam(SpamEvent::ModelNotFound));
diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs
index 9be7e92d..d8a9a50c 100644
--- a/crates/dav-proto/src/responses/property.rs
+++ b/crates/dav-proto/src/responses/property.rs
@@ -59,7 +59,7 @@ impl Display for Rfc1123DateTime {
let dt = DateTime::from_timestamp(self.0);
write!(
f,
- "{}, {} {} {:04} {:02}:{:02}:{:02} GMT",
+ "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
DOW[dt.day_of_week() as usize],
dt.day,
MONTH
diff --git a/crates/dav/src/calendar/freebusy.rs b/crates/dav/src/calendar/freebusy.rs
index 051bd02e..0a3927e6 100644
--- a/crates/dav/src/calendar/freebusy.rs
+++ b/crates/dav/src/calendar/freebusy.rs
@@ -107,7 +107,7 @@ impl CalendarFreebusyRequestHandler for Server {
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
resources
- .shared_containers(
+ .shared_items(
access_token,
[Acl::ReadItems, Acl::SchedulingReadFreeBusy],
false,
diff --git a/crates/dav/src/calendar/query.rs b/crates/dav/src/calendar/query.rs
index dbe05be7..d8a448e8 100644
--- a/crates/dav/src/calendar/query.rs
+++ b/crates/dav/src/calendar/query.rs
@@ -90,7 +90,7 @@ impl CalendarQueryRequestHandler for Server {
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
resources
- .shared_containers(access_token, [Acl::ReadItems], false)
+ .shared_items(access_token, [Acl::ReadItems], false)
.into()
} else {
None
diff --git a/crates/dav/src/card/query.rs b/crates/dav/src/card/query.rs
index 7025469d..231ebf86 100644
--- a/crates/dav/src/card/query.rs
+++ b/crates/dav/src/card/query.rs
@@ -77,7 +77,7 @@ impl CardQueryRequestHandler for Server {
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
resources
- .shared_containers(access_token, [Acl::ReadItems], false)
+ .shared_items(access_token, [Acl::ReadItems], false)
.into()
} else {
None
diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs
index ae7c3443..ef8496ed 100644
--- a/crates/http/src/request.rs
+++ b/crates/http/src/request.rs
@@ -595,7 +595,9 @@ impl ParseHttp for Server {
}
external => {
if path.next().is_none() {
- return Ok(HttpResponse::redirect(format!("/{external}/")));
+ if !external.is_empty() {
+ return Ok(HttpResponse::redirect(format!("/{external}/")));
+ }
} else if let Some(resource) = self
.inner
.data
diff --git a/tests/src/webdav/acl.rs b/tests/src/webdav/acl.rs
index b652b698..13498c1f 100644
--- a/tests/src/webdav/acl.rs
+++ b/tests/src/webdav/acl.rs
@@ -131,6 +131,11 @@ pub async fn test(test: &TestServer) {
.properties(&owner_file)
.with_status(StatusCode::OK)
.is_defined(DavProperty::WebDav(WebDavProperty::GetETag));
+ sharee_client
+ .request("REPORT", &owner_folder, CALENDAR_QUERY_ANY_VEVENT)
+ .await
+ .with_status(StatusCode::MULTI_STATUS)
+ .with_hrefs([owner_file.as_str()]);
}
DavResourceName::Card => {
sharee_client
@@ -139,6 +144,11 @@ pub async fn test(test: &TestServer) {
.properties(&owner_file)
.with_status(StatusCode::OK)
.is_defined(DavProperty::WebDav(WebDavProperty::GetETag));
+ sharee_client
+ .request("REPORT", &owner_folder, ADDRESSBOOK_QUERY_ANY_FN)
+ .await
+ .with_status(StatusCode::MULTI_STATUS)
+ .with_hrefs([owner_file.as_str()]);
}
_ => {}
}
@@ -408,3 +418,21 @@ const ACL_PRINCIPAL_QUERY: &str = r#"
"#;
+
+const CALENDAR_QUERY_ANY_VEVENT: &str = r#"
+
+
+
+
+
+
+
+ "#;
+
+const ADDRESSBOOK_QUERY_ANY_FN: &str = r#"
+
+
+
+
+
+ "#;