CardDAV working with Thunderbird and Apple Contacts

This commit is contained in:
mdecimus 2025-04-18 13:55:55 +02:00
parent ce27cecded
commit 10ae19f2eb
53 changed files with 1544 additions and 1233 deletions

1
Cargo.lock generated
View file

@ -3073,6 +3073,7 @@ dependencies = [
"directory",
"email",
"form-data",
"groupware",
"http-body-util",
"http_proto",
"hyper 1.6.0",

View file

@ -16,6 +16,10 @@ pub struct DavConfig {
pub max_changes: usize,
pub max_match_results: usize,
pub max_vcard_size: usize,
pub default_calendar_name: Option<String>,
pub default_addressbook_name: Option<String>,
pub default_calendar_display_name: Option<String>,
pub default_addressbook_display_name: Option<String>,
}
impl DavConfig {
@ -41,6 +45,24 @@ impl DavConfig {
max_vcard_size: config
.property("dav.limits.size.vcard")
.unwrap_or(512 * 1024),
default_calendar_name: config
.property_or_default::<Option<String>>("dav.default.calendar.name", "default")
.unwrap_or_default(),
default_addressbook_name: config
.property_or_default::<Option<String>>("dav.default.addressbook.name", "default")
.unwrap_or_default(),
default_calendar_display_name: config
.property_or_default::<Option<String>>(
"dav.default.calendar.display-name",
"Default Calendar",
)
.unwrap_or_default(),
default_addressbook_display_name: config
.property_or_default::<Option<String>>(
"dav.default.addressbook.display-name",
"Default Address Book",
)
.unwrap_or_default(),
}
}
}

View file

@ -242,6 +242,7 @@ pub struct DavResourceId {
#[derive(Debug, Default)]
pub struct DavResources {
pub base_path: String,
pub paths: IdBimap<DavResource>,
pub size: u64,
pub modseq: Option<u64>,
@ -479,7 +480,7 @@ impl DavResources {
self.paths.iter().filter(move |item| {
item.name
.strip_prefix(&prefix)
.is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth)
.is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() < depth)
|| item.name == search_path
})
}
@ -503,6 +504,22 @@ impl DavResources {
let prefix = format!("{ancestor}/");
descendant.starts_with(&prefix) || descendant == ancestor
}
pub fn format_resource(&self, resource: &DavResource) -> String {
if resource.is_container {
format!("{}{}/", self.base_path, resource.name)
} else {
format!("{}{}", self.base_path, resource.name)
}
}
pub fn format_collection(&self, name: &str) -> String {
format!("{}{name}/", self.base_path)
}
pub fn format_item(&self, name: &str) -> String {
format!("{}{}", self.base_path, name)
}
}
impl IdBimapItem for DavResource {

View file

@ -9,10 +9,29 @@ pub mod requests;
pub mod responses;
pub mod schema;
pub fn xml_pretty_print(xml_string: &str) -> String {
// Create a reader
let mut reader = quick_xml::Reader::from_str(xml_string);
let mut writer = quick_xml::Writer::new_with_indent(std::io::Cursor::new(Vec::new()), b' ', 2);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(quick_xml::events::Event::Eof) => break,
Ok(event) => {
writer.write_event(event).unwrap();
}
Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
}
buf.clear();
}
let result = writer.into_inner().into_inner();
String::from_utf8(result).unwrap()
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct RequestHeaders<'x> {
pub uri: &'x str,
pub base_uri: Option<&'x str>,
pub depth: Depth,
pub timeout: Timeout,
pub content_type: Option<&'x str>,

View file

@ -10,7 +10,6 @@ impl<'x> RequestHeaders<'x> {
pub fn new(uri: &'x str) -> Self {
RequestHeaders {
uri,
base_uri: base_uri(uri),
..Default::default()
}
}
@ -89,11 +88,6 @@ impl<'x> RequestHeaders<'x> {
false
}
pub fn format_to_base_uri(&self, path: &str) -> String {
let base_uri = self.base_uri.unwrap_or_default();
format!("{base_uri}/{path}")
}
pub fn has_if(&self) -> bool {
!self.if_.is_empty()
}
@ -260,9 +254,13 @@ impl<'x> RequestHeaders<'x> {
});
}
}
pub fn base_uri(&self) -> Option<&str> {
dav_base_uri(self.uri)
}
}
fn base_uri(uri: &str) -> Option<&str> {
pub fn dav_base_uri(uri: &str) -> Option<&str> {
// From a path ../dav/collection/account/..
// returns ../dav/collection/account without the trailing slash
@ -359,7 +357,7 @@ mod tests {
("/dav/collection/account/", Some("/dav/collection/account")),
("/dav/collection/account", Some("/dav/collection/account")),
] {
assert_eq!(RequestHeaders::new(uri).base_uri, expected_base);
assert_eq!(RequestHeaders::new(uri).base_uri(), expected_base);
}
}

View file

@ -72,6 +72,13 @@ impl NamedElement {
element,
}
}
pub fn calendarserver(element: Element) -> NamedElement {
NamedElement {
ns: Namespace::CalendarServer,
element,
}
}
}
impl Token<'_> {

View file

@ -542,6 +542,9 @@ impl DavProperty {
(Namespace::CalDav, Element::CalendarTimezoneId) => {
Some(DavProperty::CalDav(CalDavProperty::TimezoneId))
}
(Namespace::CalendarServer, Element::Getctag) => {
Some(DavProperty::WebDav(WebDavProperty::GetCTag))
}
_ => None,
}
}

View file

@ -14,7 +14,7 @@ use crate::{
Ace, AclRestrictions, GrantDeny, Href, List, Principal, PrincipalSearchProperty,
PrincipalSearchPropertySet, RequiredPrincipal, Resource, SupportedPrivilege,
},
Namespace,
Namespace, Namespaces,
},
};
@ -175,7 +175,7 @@ impl Display for Privilege {
Privilege::Bind => "<D:privilege><D:bind/></D:privilege>".fmt(f),
Privilege::Unbind => "<D:privilege><D:unbind/></D:privilege>".fmt(f),
Privilege::All => "<D:privilege><D:all/></D:privilege>".fmt(f),
Privilege::ReadFreeBusy => "<D:privilege><C:read-free-busy/></D:privilege>".fmt(f),
Privilege::ReadFreeBusy => "<D:privilege><A:read-free-busy/></D:privilege>".fmt(f),
}
}
}
@ -195,7 +195,7 @@ impl Display for PrincipalSearchPropertySet {
write!(
f,
"<D:principal-search-property-set {}>{}</D:principal-search-property-set>",
self.namespace, self.properties
self.namespaces, self.properties
)
}
}
@ -227,13 +227,13 @@ impl Resource {
impl PrincipalSearchPropertySet {
pub fn new(properties: Vec<PrincipalSearchProperty>) -> Self {
PrincipalSearchPropertySet {
namespace: Namespace::Dav,
namespaces: Namespaces::default(),
properties: List(properties),
}
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespace = namespace;
self.namespaces.set(namespace);
self
}
}

View file

@ -8,12 +8,12 @@ use std::fmt::Display;
use crate::schema::{
response::{BaseCondition, CalCondition, CardCondition, Condition, ErrorResponse},
Namespace,
Namespace, Namespaces,
};
impl Display for ErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:error {}>", self.namespace)?;
write!(f, "<D:error {}>", self.namespaces)?;
match &self.error {
Condition::Base(e) => e.fmt(f)?,
@ -88,31 +88,31 @@ impl Display for CalCondition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CalCondition::CalendarCollectionLocationOk => {
write!(f, "<C:calendar-collection-location-ok/>")
write!(f, "<A:calendar-collection-location-ok/>")
}
CalCondition::ValidCalendarData => write!(f, "<C:valid-calendar-data/>"),
CalCondition::ValidFilter => write!(f, "<C:valid-filter/>"),
CalCondition::ValidCalendarData => write!(f, "<A:valid-calendar-data/>"),
CalCondition::ValidFilter => write!(f, "<A:valid-filter/>"),
CalCondition::ValidCalendarObjectResource => {
write!(f, "<C:valid-calendar-object-resource/>")
write!(f, "<A:valid-calendar-object-resource/>")
}
CalCondition::NoUidConflict(uid) => {
write!(f, "<C:no-uid-conflict>{uid}</C:no-uid-conflict>")
write!(f, "<A:no-uid-conflict>{uid}</A:no-uid-conflict>")
}
CalCondition::InitializeCalendarCollection => {
write!(f, "<C:initialize-calendar-collection/>")
write!(f, "<A:initialize-calendar-collection/>")
}
CalCondition::SupportedCalendarData => write!(f, "<C:supported-calendar-data/>"),
CalCondition::SupportedFilter(_) => write!(f, "<C:supported-filter/>"),
CalCondition::SupportedCalendarData => write!(f, "<A:supported-calendar-data/>"),
CalCondition::SupportedFilter(_) => write!(f, "<A:supported-filter/>"),
CalCondition::SupportedCollation(c) => {
write!(f, "<C:supported-collation>{c}</C:supported-collation>")
write!(f, "<A:supported-collation>{c}</A:supported-collation>")
}
CalCondition::MinDateTime => write!(f, "<C:min-date-time/>"),
CalCondition::MaxDateTime => write!(f, "<C:max-date-time/>"),
CalCondition::MinDateTime => write!(f, "<A:min-date-time/>"),
CalCondition::MaxDateTime => write!(f, "<A:max-date-time/>"),
CalCondition::MaxResourceSize(l) => {
write!(f, "<C:max-resource-size>{l}</C:max-resource-size>")
write!(f, "<A:max-resource-size>{l}</A:max-resource-size>")
}
CalCondition::MaxInstances => write!(f, "<C:max-instances/>"),
CalCondition::MaxAttendeesPerInstance => write!(f, "<C:max-attendees-per-instance/>"),
CalCondition::MaxInstances => write!(f, "<A:max-instances/>"),
CalCondition::MaxAttendeesPerInstance => write!(f, "<A:max-attendees-per-instance/>"),
}
}
}
@ -120,23 +120,23 @@ impl Display for CalCondition {
impl Display for CardCondition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CardCondition::SupportedAddressData => write!(f, "<C:supported-address-data/>"),
CardCondition::SupportedAddressData => write!(f, "<B:supported-address-data/>"),
CardCondition::SupportedAddressDataConversion => {
write!(f, "<C:supported-address-data-conversion/>")
write!(f, "<B:supported-address-data-conversion/>")
}
CardCondition::SupportedFilter(_) => write!(f, "<C:supported-filter/>"),
CardCondition::SupportedFilter(_) => write!(f, "<B:supported-filter/>"),
CardCondition::SupportedCollation(c) => {
write!(f, "<C:supported-collation>{c}</C:supported-collation>")
write!(f, "<B:supported-collation>{c}</B:supported-collation>")
}
CardCondition::ValidAddressData => write!(f, "<C:valid-address-data/>"),
CardCondition::ValidAddressData => write!(f, "<B:valid-address-data/>"),
CardCondition::NoUidConflict(uid) => {
write!(f, "<C:no-uid-conflict>{uid}</C:no-uid-conflict>")
write!(f, "<B:no-uid-conflict>{uid}</B:no-uid-conflict>")
}
CardCondition::MaxResourceSize(l) => {
write!(f, "<C:max-resource-size>{l}</C:max-resource-size>")
write!(f, "<B:max-resource-size>{l}</B:max-resource-size>")
}
CardCondition::AddressBookCollectionLocationOk => {
write!(f, "<C:addressbook-collection-location-ok/>")
write!(f, "<B:addressbook-collection-location-ok/>")
}
}
}
@ -163,13 +163,13 @@ impl From<BaseCondition> for Condition {
impl ErrorResponse {
pub fn new(error: impl Into<Condition>) -> Self {
ErrorResponse {
namespace: Namespace::Dav,
namespaces: Namespaces::default(),
error: error.into(),
}
}
pub fn with_namespace(mut self, namespace: impl Into<Namespace>) -> Self {
self.namespace = namespace.into();
self.namespaces.set(namespace.into());
self
}
}

View file

@ -8,7 +8,7 @@ use std::fmt::Display;
use crate::schema::{
response::{List, MkColResponse, PropStat},
Namespace,
Namespace, Namespaces,
};
impl Display for MkColResponse {
@ -16,7 +16,7 @@ impl Display for MkColResponse {
write!(
f,
"<D:mkcol-response {}>{}</D:mkcol-response>",
self.namespace, self.propstat
self.namespaces, self.propstat
)
}
}
@ -24,13 +24,13 @@ impl Display for MkColResponse {
impl MkColResponse {
pub fn new(propstat: Vec<PropStat>) -> Self {
Self {
namespace: Namespace::Dav,
namespaces: Namespaces::default(),
propstat: List(propstat),
}
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespace = namespace;
self.namespaces.set(namespace);
self
}
}

View file

@ -18,7 +18,7 @@ use crate::schema::{
property::{Comp, ResourceType, SupportedCollation},
request::{DeadProperty, DeadPropertyTag},
response::{Href, List, Location, ResponseDescription, Status, SyncToken},
Namespace,
Namespaces,
};
trait XmlEscape {
@ -44,19 +44,19 @@ impl<T: AsRef<str>> XmlEscape for T {
}
}
impl Namespace {
pub(crate) fn write_to(&self, out: &mut impl Write) -> std::fmt::Result {
out.write_str(match self {
Namespace::Dav => "xmlns:D=\"DAV:\"",
Namespace::CalDav => "xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\"",
Namespace::CardDav => "xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:carddav\"",
})
}
}
impl Display for Namespace {
impl Display for Namespaces {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.write_to(f)
f.write_str("xmlns:D=\"DAV:\"")?;
if self.cal {
f.write_str(" xmlns:A=\"urn:ietf:params:xml:ns:caldav\"")?;
}
if self.card {
f.write_str(" xmlns:B=\"urn:ietf:params:xml:ns:carddav\"")?;
}
if self.cs {
f.write_str(" xmlns:C=\"http://calendarserver.org/ns/\"")?;
}
Ok(())
}
}
@ -112,7 +112,7 @@ impl Display for SyncToken {
impl Display for Comp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<C:comp name=\"{}\">", self.0.as_str())
write!(f, "<A:comp name=\"{}\">", self.0.as_str())
}
}
@ -121,18 +121,19 @@ impl Display for ResourceType {
match self {
ResourceType::Collection => write!(f, "<D:collection/>"),
ResourceType::Principal => write!(f, "<D:principal/>"),
ResourceType::AddressBook => write!(f, "<C:addressbook/>"),
ResourceType::Calendar => write!(f, "<C:calendar/>"),
ResourceType::AddressBook => write!(f, "<B:addressbook/>"),
ResourceType::Calendar => write!(f, "<A:calendar/>"),
}
}
}
impl Display for SupportedCollation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ns = self.namespace.prefix();
write!(
f,
"<C:supported-collation>{}</C:supported-collation>",
self.0.as_str()
"<{ns}:supported-collation>{}</{ns}:supported-collation>",
self.collation.as_str()
)
}
}

View file

@ -13,12 +13,12 @@ use crate::schema::{
Condition, Href, List, Location, MultiStatus, PropStat, Response, ResponseDescription,
ResponseType, Status, SyncToken,
},
Namespace,
Namespace, Namespaces,
};
impl Display for MultiStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:multistatus {}>{}", self.namespace, self.response)?;
write!(f, "<D:multistatus {}>{}", self.namespaces, self.response)?;
if let Some(response_description) = &self.response_description {
write!(f, "{response_description}")?;
}
@ -64,7 +64,7 @@ impl Display for ResponseType {
impl MultiStatus {
pub fn new(response: Vec<Response>) -> Self {
MultiStatus {
namespace: Namespace::Dav,
namespaces: Namespaces::default(),
response: List(response),
response_description: None,
sync_token: None,
@ -86,12 +86,12 @@ impl MultiStatus {
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespace = namespace;
self.namespaces.set(namespace);
self
}
pub fn set_namespace(&mut self, namespace: Namespace) {
self.namespace = namespace;
self.namespaces.set(namespace);
}
pub fn with_sync_token(mut self, sync_token: impl Into<String>) -> Self {

View file

@ -20,14 +20,18 @@ use crate::schema::{
},
request::{DavPropertyValue, DeadProperty},
response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege},
Namespace,
Namespace, Namespaces,
};
use super::XmlEscape;
impl Display for PropResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:prop {}>{}</D:prop>", self.namespace, self.properties)
write!(
f,
"<D:prop {}>{}</D:prop>",
self.namespaces, self.properties
)
}
}
@ -98,11 +102,11 @@ impl Display for DavValue {
write!(
f,
concat!(
"<C:supported-address-data>",
"<C:address-data-type content-type=\"text/vcard\" version=\"4.0\"/>",
"<C:address-data-type content-type=\"text/vcard\" version=\"3.0\"/>",
"<C:address-data-type content-type=\"text/vcard\" version=\"2.0\"/>",
"</C:supported-address-data>"
"<B:supported-address-data>",
"<B:address-data-type content-type=\"text/vcard\" version=\"4.0\"/>",
"<B:address-data-type content-type=\"text/vcard\" version=\"3.0\"/>",
"<B:address-data-type content-type=\"text/vcard\" version=\"2.0\"/>",
"</B:supported-address-data>"
)
)
}
@ -110,10 +114,10 @@ impl Display for DavValue {
write!(
f,
concat!(
"<C:supported-calendar-data>",
"<C:calendar-data-type content-type=\"text/calendar\" version=\"2.0\"/>",
"<C:calendar-data-type content-type=\"text/calendar\" version=\"1.0\"/>",
"</C:supported-calendar-data>"
"<A:supported-calendar-data>",
"<A:calendar-data-type content-type=\"text/calendar\" version=\"2.0\"/>",
"<A:calendar-data-type content-type=\"text/calendar\" version=\"1.0\"/>",
"</A:supported-calendar-data>"
)
)
}
@ -150,39 +154,40 @@ impl DavProperty {
WebDavProperty::AclRestrictions => "D:acl-restrictions",
WebDavProperty::InheritedAclSet => "D:inherited-acl-set",
WebDavProperty::PrincipalCollectionSet => "D:principal-collection-set",
WebDavProperty::GetCTag => "C:getctag",
},
DavProperty::CardDav(prop) => match prop {
CardDavProperty::AddressbookDescription => "C:addressbook-description",
CardDavProperty::SupportedAddressData => "C:supported-address-data",
CardDavProperty::SupportedCollationSet => "C:supported-collation-set",
CardDavProperty::MaxResourceSize => "C:max-resource-size",
CardDavProperty::AddressData(_) => "C:address-data",
CardDavProperty::AddressbookDescription => "B:addressbook-description",
CardDavProperty::SupportedAddressData => "B:supported-address-data",
CardDavProperty::SupportedCollationSet => "B:supported-collation-set",
CardDavProperty::MaxResourceSize => "B:max-resource-size",
CardDavProperty::AddressData(_) => "B:address-data",
},
DavProperty::CalDav(prop) => match prop {
CalDavProperty::CalendarDescription => "C:calendar-description",
CalDavProperty::CalendarTimezone => "C:calendar-timezone",
CalDavProperty::CalendarDescription => "A:calendar-description",
CalDavProperty::CalendarTimezone => "A:calendar-timezone",
CalDavProperty::SupportedCalendarComponentSet => {
"C:supported-calendar-component-set"
"A:supported-calendar-component-set"
}
CalDavProperty::SupportedCalendarData => "C:supported-calendar-data",
CalDavProperty::SupportedCollationSet => "C:supported-collation-set",
CalDavProperty::MaxResourceSize => "C:max-resource-size",
CalDavProperty::MinDateTime => "C:min-date-time",
CalDavProperty::MaxDateTime => "C:max-date-time",
CalDavProperty::MaxInstances => "C:max-instances",
CalDavProperty::MaxAttendeesPerInstance => "C:max-attendees-per-instance",
CalDavProperty::CalendarHomeSet => "C:calendar-home-set",
CalDavProperty::CalendarData(_) => "C:calendar-data",
CalDavProperty::TimezoneServiceSet => "C:timezone-service-set",
CalDavProperty::TimezoneId => "C:calendar-timezone-id",
CalDavProperty::SupportedCalendarData => "A:supported-calendar-data",
CalDavProperty::SupportedCollationSet => "A:supported-collation-set",
CalDavProperty::MaxResourceSize => "A:max-resource-size",
CalDavProperty::MinDateTime => "A:min-date-time",
CalDavProperty::MaxDateTime => "A:max-date-time",
CalDavProperty::MaxInstances => "A:max-instances",
CalDavProperty::MaxAttendeesPerInstance => "A:max-attendees-per-instance",
CalDavProperty::CalendarHomeSet => "A:calendar-home-set",
CalDavProperty::CalendarData(_) => "A:calendar-data",
CalDavProperty::TimezoneServiceSet => "A:timezone-service-set",
CalDavProperty::TimezoneId => "A:calendar-timezone-id",
},
DavProperty::Principal(prop) => match prop {
PrincipalProperty::AlternateURISet => "D:alternate-URI-set",
PrincipalProperty::PrincipalURL => "D:principal-URL",
PrincipalProperty::GroupMemberSet => "D:group-member-set",
PrincipalProperty::GroupMembership => "D:group-membership",
PrincipalProperty::AddressbookHomeSet => "C:addressbook-home-set",
PrincipalProperty::PrincipalAddress => "C:principal-address",
PrincipalProperty::AddressbookHomeSet => "B:addressbook-home-set",
PrincipalProperty::PrincipalAddress => "B:principal-address",
},
DavProperty::DeadProperty(dead) => {
return (dead.name.as_str(), dead.attrs.as_deref())
@ -195,21 +200,23 @@ impl DavProperty {
impl Display for ReportSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<D:supported-report><D:report>")?;
match self {
ReportSet::SyncCollection => write!(f, "<D:sync-collection/>"),
ReportSet::ExpandProperty => write!(f, "<D:expand-property/>"),
ReportSet::AddressbookQuery => write!(f, "<C:addressbook-query/>"),
ReportSet::AddressbookMultiGet => write!(f, "<C:addressbook-multiget/>"),
ReportSet::CalendarQuery => write!(f, "<C:calendar-query/>"),
ReportSet::CalendarMultiGet => write!(f, "<C:calendar-multiget/>"),
ReportSet::FreeBusyQuery => write!(f, "<C:free-busy-query/>"),
ReportSet::AddressbookQuery => write!(f, "<B:addressbook-query/>"),
ReportSet::AddressbookMultiGet => write!(f, "<B:addressbook-multiget/>"),
ReportSet::CalendarQuery => write!(f, "<A:calendar-query/>"),
ReportSet::CalendarMultiGet => write!(f, "<A:calendar-multiget/>"),
ReportSet::FreeBusyQuery => write!(f, "<A:free-busy-query/>"),
ReportSet::AclPrincipalPropSet => write!(f, "<D:acl-principal-prop-set/>"),
ReportSet::PrincipalMatch => write!(f, "<D:principal-match/>"),
ReportSet::PrincipalPropertySearch => write!(f, "<D:principal-property-search/>"),
ReportSet::PrincipalSearchPropertySet => {
write!(f, "<D:principal-search-property-set/>")
}
}
}?;
f.write_str("</D:report></D:supported-report>")
}
}
@ -227,13 +234,13 @@ impl Display for DavProperty {
impl PropResponse {
pub fn new(properties: Vec<DavPropertyValue>) -> Self {
PropResponse {
namespace: Namespace::Dav,
namespaces: Namespaces::default(),
properties: List(properties),
}
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespace = namespace;
self.namespaces.set(namespace);
self
}
}

View file

@ -20,10 +20,31 @@ pub struct NamedElement {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[repr(u8)]
pub enum Namespace {
Dav,
CalDav,
CardDav,
CalendarServer,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct Namespaces {
pub(crate) cal: bool,
pub(crate) card: bool,
pub(crate) cs: bool,
}
impl Namespaces {
pub fn set(&mut self, ns: Namespace) {
match ns {
Namespace::CalDav => self.cal = true,
Namespace::CardDav => self.card = true,
Namespace::CalendarServer => self.cs = true,
Namespace::Dav => {}
}
}
}
impl Namespace {
@ -31,9 +52,20 @@ impl Namespace {
hashify::tiny_map!(value,
"DAV:" => Namespace::Dav,
"urn:ietf:params:xml:ns:caldav" => Namespace::CalDav,
"urn:ietf:params:xml:ns:carddav" => Namespace::CardDav
"urn:ietf:params:xml:ns:carddav" => Namespace::CardDav,
"http://calendarserver.org/ns/" => Namespace::CalendarServer,
"http://calendarserver.org/ns" => Namespace::CalendarServer
)
}
pub fn prefix(&self) -> &str {
match self {
Namespace::Dav => "D",
Namespace::CalDav => "A",
Namespace::CardDav => "B",
Namespace::CalendarServer => "C",
}
}
}
impl AsRef<str> for Namespace {
@ -42,6 +74,7 @@ impl AsRef<str> for Namespace {
Namespace::Dav => "DAV:",
Namespace::CalDav => "urn:ietf:params:xml:ns:caldav",
Namespace::CardDav => "urn:ietf:params:xml:ns:carddav",
Namespace::CalendarServer => "http://calendarserver.org/ns/",
}
}
}
@ -163,6 +196,7 @@ pub enum Element {
Getcontentlanguage,
Getcontentlength,
Getcontenttype,
Getctag,
Getetag,
Getlastmodified,
Grammar,
@ -543,6 +577,7 @@ impl Element {
"getcontentlength" => Element::Getcontentlength,
"getcontenttype" => Element::Getcontenttype,
"getetag" => Element::Getetag,
"getctag" => Element::Getctag,
"getlastmodified" => Element::Getlastmodified,
"grammar" => Element::Grammar,
"grant" => Element::Grant,
@ -927,6 +962,7 @@ impl AsRef<str> for Element {
Element::Getcontentlength => "getcontentlength",
Element::Getcontenttype => "getcontenttype",
Element::Getetag => "getetag",
Element::Getctag => "getctag",
Element::Getlastmodified => "getlastmodified",
Element::Grammar => "grammar",
Element::Grant => "grant",

View file

@ -14,7 +14,7 @@ use crate::{Depth, Timeout};
use super::{
request::{DavPropertyValue, DeadElementTag, DeadProperty},
response::{Ace, AclRestrictions, Href, List, SupportedPrivilege},
Collation,
Collation, Namespace,
};
#[derive(Debug, Clone, PartialEq, Eq)]
@ -58,6 +58,8 @@ pub enum WebDavProperty {
AclRestrictions,
InheritedAclSet,
PrincipalCollectionSet,
// Apple proprietary properties
GetCTag,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -188,7 +190,10 @@ pub struct Comp(pub ICalendarComponentType);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct SupportedCollation(pub Collation);
pub struct SupportedCollation {
pub collation: Collation,
pub namespace: Namespace,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
@ -257,6 +262,41 @@ pub enum Privilege {
ReadFreeBusy,
}
impl Privilege {
pub fn all(is_calendar: bool) -> Vec<Privilege> {
if is_calendar {
vec![
Privilege::All,
Privilege::Read,
Privilege::Write,
Privilege::WriteProperties,
Privilege::WriteContent,
Privilege::Unlock,
Privilege::ReadAcl,
Privilege::ReadCurrentUserPrivilegeSet,
Privilege::WriteAcl,
Privilege::Bind,
Privilege::Unbind,
Privilege::ReadFreeBusy,
]
} else {
vec![
Privilege::All,
Privilege::Read,
Privilege::Write,
Privilege::WriteProperties,
Privilege::WriteContent,
Privilege::Unlock,
Privilege::ReadAcl,
Privilege::ReadCurrentUserPrivilegeSet,
Privilege::WriteAcl,
Privilege::Bind,
Privilege::Unbind,
]
}
}
}
impl From<DavProperty> for DavPropertyValue {
fn from(value: DavProperty) -> Self {
DavPropertyValue {
@ -303,3 +343,44 @@ impl DavProperty {
)
}
}
impl ReportSet {
pub fn calendar() -> Vec<ReportSet> {
vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
ReportSet::ExpandProperty,
ReportSet::CalendarQuery,
ReportSet::CalendarMultiGet,
ReportSet::FreeBusyQuery,
]
}
pub fn addressbook() -> Vec<ReportSet> {
vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
ReportSet::ExpandProperty,
ReportSet::AddressbookQuery,
ReportSet::AddressbookMultiGet,
]
}
pub fn file() -> Vec<ReportSet> {
vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
]
}
pub fn principal() -> Vec<ReportSet> {
vec![
ReportSet::PrincipalPropertySearch,
ReportSet::PrincipalSearchPropertySet,
ReportSet::PrincipalMatch,
]
}
}

View file

@ -15,11 +15,11 @@ use hyper::StatusCode;
use super::{
property::{DavProperty, Privilege},
request::{DavPropertyValue, Filter},
Namespace,
Namespaces,
};
pub struct MultiStatus {
pub namespace: Namespace,
pub namespaces: Namespaces,
pub response: List<Response>,
pub response_description: Option<ResponseDescription>,
pub sync_token: Option<SyncToken>,
@ -61,7 +61,7 @@ pub struct Href(pub String);
pub struct List<T: Display>(pub Vec<T>);
pub struct MkColResponse {
pub namespace: Namespace,
pub namespaces: Namespaces,
pub propstat: List<PropStat>,
}
@ -76,7 +76,7 @@ pub struct PropStat {
pub struct Prop(pub List<DavPropertyValue>);
pub struct PropResponse {
pub namespace: Namespace,
pub namespaces: Namespaces,
pub properties: List<DavPropertyValue>,
}
@ -141,7 +141,7 @@ pub enum RequiredPrincipal {
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PrincipalSearchPropertySet {
pub namespace: Namespace,
pub namespaces: Namespaces,
pub properties: List<PrincipalSearchProperty>,
}
@ -153,7 +153,7 @@ pub struct PrincipalSearchProperty {
}
pub struct ErrorResponse {
pub namespace: Namespace,
pub namespaces: Namespaces,
pub error: Condition,
}

View file

@ -7,7 +7,7 @@
use common::{Server, auth::AccessToken};
use dav_proto::{Depth, RequestHeaders, schema::response::CardCondition};
use groupware::{
DavName,
DavName, DestroyArchive,
contact::{AddressBook, ContactCard},
hierarchy::DavHierarchy,
};
@ -17,18 +17,9 @@ use jmap_proto::types::{acl::Acl, collection::Collection};
use store::write::BatchBuilder;
use trc::AddContext;
use crate::{
DavError, DavErrorCondition,
card::{delete::delete_address_book, insert_addressbook, insert_card, update_card},
common::uri::DavUriResource,
file::DavFileResource,
};
use crate::{DavError, DavErrorCondition, common::uri::DavUriResource, file::DavFileResource};
use super::{
assert_is_unique_uid,
delete::{delete_address_book_and_cards, delete_card},
update_addressbook,
};
use super::assert_is_unique_uid;
pub(crate) trait CardCopyMoveRequestHandler: Sync + Send {
fn handle_card_copy_move_request(
@ -53,7 +44,7 @@ impl CardCopyMoveRequestHandler for Server {
.into_owned_uri()?;
let from_account_id = from_resource_.account_id;
let from_resources = self
.fetch_dav_resources(from_account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, from_account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?;
let from_resource_name = from_resource_
@ -102,7 +93,7 @@ impl CardCopyMoveRequestHandler for Server {
let to_resources = if to_account_id == from_account_id {
from_resources.clone()
} else {
self.fetch_dav_resources(to_account_id, Collection::AddressBook)
self.fetch_dav_resources(access_token, to_account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?
};
@ -174,12 +165,6 @@ impl CardCopyMoveRequestHandler for Server {
// Overwrite card
let from_addressbook_id = from_resource.parent_id.unwrap();
let to_addressbook_id = to_resource.parent_id.unwrap();
let to_base_path = headers.format_to_base_uri(
destination_resource_name
.rsplit_once('/')
.map(|(base, _)| base)
.unwrap_or(destination_resource_name),
);
// Validate ACL
if (!access_token.is_member(from_account_id)
@ -212,6 +197,12 @@ impl CardCopyMoveRequestHandler for Server {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let to_base_path = to_resources
.format_resource(to_resource)
.rsplit_once('/')
.unwrap()
.0
.to_string();
if is_move {
move_card(
self,
@ -300,7 +291,7 @@ impl CardCopyMoveRequestHandler for Server {
to_account_id,
None,
to_addressbook_id,
headers.format_to_base_uri(&parent_resource.name),
to_resources.format_resource(parent_resource),
new_name,
)
.await
@ -324,7 +315,7 @@ impl CardCopyMoveRequestHandler for Server {
from_addressbook_id,
None,
to_addressbook_id,
headers.format_to_base_uri(&parent_resource.name),
to_resources.format_resource(parent_resource),
new_name,
)
.await
@ -453,7 +444,7 @@ async fn copy_card(
{
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(format!("{}/{}", to_base_path, name.name).into()),
CardCondition::NoUidConflict(format!("{}{}", to_base_path, name.name).into()),
)));
}
let mut new_card = card
@ -463,29 +454,27 @@ async fn copy_card(
name: new_name.to_string(),
parent_id: to_addressbook_id,
});
update_card(
access_token,
card,
new_card,
from_account_id,
from_document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_card
.update(
access_token,
card,
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?;
} else {
// Validate UID
assert_is_unique_uid(
server,
server
.fetch_dav_resources(to_account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, to_account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?
.as_ref(),
to_account_id,
to_addressbook_id,
card.inner.card.uid(),
&to_base_path,
)
.await?;
@ -501,15 +490,9 @@ async fn copy_card(
.assign_document_ids(to_account_id, Collection::ContactCard, 1)
.await
.caused_by(trc::location!())?;
insert_card(
access_token,
new_card,
to_account_id,
to_document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_card
.insert(access_token, to_account_id, to_document_id, &mut batch)
.caused_by(trc::location!())?;
}
let response = if let Some(to_document_id) = to_document_id {
@ -523,15 +506,15 @@ async fn copy_card(
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?;
delete_card(
access_token,
to_account_id,
to_document_id,
to_addressbook_id,
card,
&mut batch,
)
.caused_by(trc::location!())?;
DestroyArchive(card)
.delete(
access_token,
to_account_id,
to_document_id,
to_addressbook_id,
&mut batch,
)
.caused_by(trc::location!())?;
}
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
@ -577,7 +560,7 @@ async fn move_card(
if name.parent_id == to_addressbook_id {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(format!("{}/{}", to_base_path, name.name).into()),
CardCondition::NoUidConflict(format!("{}{}", to_base_path, name.name).into()),
)));
} else if name.parent_id == from_addressbook_id {
name_idx = Some(idx);
@ -599,29 +582,27 @@ async fn move_card(
name: new_name.to_string(),
parent_id: to_addressbook_id,
});
update_card(
access_token,
card.clone(),
new_card,
from_account_id,
from_document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_card
.update(
access_token,
card.clone(),
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?;
} else {
// Validate UID
assert_is_unique_uid(
server,
server
.fetch_dav_resources(to_account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, to_account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?
.as_ref(),
to_account_id,
to_addressbook_id,
card.inner.card.uid(),
&to_base_path,
)
.await?;
@ -633,30 +614,24 @@ async fn move_card(
parent_id: to_addressbook_id,
}];
delete_card(
access_token,
from_account_id,
from_document_id,
from_addressbook_id,
card,
&mut batch,
)
.caused_by(trc::location!())?;
DestroyArchive(card)
.delete(
access_token,
from_account_id,
from_document_id,
from_addressbook_id,
&mut batch,
)
.caused_by(trc::location!())?;
let to_document_id = server
.store()
.assign_document_ids(to_account_id, Collection::ContactCard, 1)
.await
.caused_by(trc::location!())?;
insert_card(
access_token,
new_card,
to_account_id,
to_document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_card
.insert(access_token, to_account_id, to_document_id, &mut batch)
.caused_by(trc::location!())?;
}
let response = if let Some(to_document_id) = to_document_id {
@ -670,15 +645,15 @@ async fn move_card(
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?;
delete_card(
access_token,
to_account_id,
to_document_id,
to_addressbook_id,
card,
&mut batch,
)
.caused_by(trc::location!())?;
DestroyArchive(card)
.delete(
access_token,
to_account_id,
to_document_id,
to_addressbook_id,
&mut batch,
)
.caused_by(trc::location!())?;
}
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
@ -725,16 +700,9 @@ async fn rename_card(
new_card.names[name_idx].name = new_name.to_string();
let mut batch = BatchBuilder::new();
update_card(
access_token,
card,
new_card,
account_id,
document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_card
.update(access_token, card, account_id, document_id, &mut batch)
.caused_by(trc::location!())?;
server
.commit_batch(batch)
.await
@ -773,14 +741,9 @@ async fn copy_container(
let mut batch = BatchBuilder::new();
if remove_source {
delete_address_book(
access_token,
from_account_id,
from_document_id,
old_book,
&mut batch,
)
.caused_by(trc::location!())?;
DestroyArchive(old_book)
.delete(access_token, from_account_id, from_document_id, &mut batch)
.caused_by(trc::location!())?;
}
book.name = new_name.to_string();
@ -800,17 +763,17 @@ async fn copy_container(
.to_unarchived::<AddressBook>()
.caused_by(trc::location!())?;
delete_address_book_and_cards(
server,
access_token,
to_account_id,
to_document_id,
to_children_ids,
book,
&mut batch,
)
.await
.caused_by(trc::location!())?;
DestroyArchive(book)
.delete_with_cards(
server,
access_token,
to_account_id,
to_document_id,
to_children_ids,
&mut batch,
)
.await
.caused_by(trc::location!())?;
}
to_document_id
@ -821,15 +784,8 @@ async fn copy_container(
.await
.caused_by(trc::location!())?
};
insert_addressbook(
access_token,
book,
to_account_id,
to_document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
book.insert(access_token, to_account_id, to_document_id, &mut batch)
.caused_by(trc::location!())?;
// Copy children
let mut required_space = 0;
@ -877,27 +833,26 @@ async fn copy_container(
}
new_card.names.push(new_name);
update_card(
access_token,
card,
new_card,
from_account_id,
from_document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
} else {
if remove_source {
delete_card(
new_card
.update(
access_token,
from_account_id,
from_child_document_id,
from_document_id,
card,
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?;
} else {
if remove_source {
DestroyArchive(card)
.delete(
access_token,
from_account_id,
from_child_document_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?;
}
let to_document_id = server
@ -907,15 +862,9 @@ async fn copy_container(
.caused_by(trc::location!())?;
new_card.names = vec![new_name];
required_space += new_card.size as u64;
insert_card(
access_token,
new_card,
to_account_id,
to_document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_card
.insert(access_token, to_account_id, to_document_id, &mut batch)
.caused_by(trc::location!())?;
}
}
}
@ -966,16 +915,9 @@ async fn rename_container(
new_book.name = new_name.to_string();
let mut batch = BatchBuilder::new();
update_addressbook(
access_token,
book,
new_book,
account_id,
document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_book
.update(access_token, book, account_id, document_id, &mut batch)
.caused_by(trc::location!())?;
server
.commit_batch(batch)
.await

View file

@ -4,18 +4,17 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::RequestHeaders;
use groupware::{
contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard},
DestroyArchive,
contact::{AddressBook, ContactCard},
hierarchy::DavHierarchy,
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use jmap_proto::types::{acl::Acl, collection::Collection};
use store::write::{Archive, BatchBuilder};
use store::write::BatchBuilder;
use trc::AddContext;
use crate::{
@ -52,7 +51,7 @@ impl CardDeleteRequestHandler for Server {
.filter(|r| !r.is_empty())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let resources = self
.fetch_dav_resources(account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?;
@ -105,21 +104,21 @@ impl CardDeleteRequestHandler for Server {
.await?;
// Delete addressbook and cards
delete_address_book_and_cards(
self,
access_token,
account_id,
document_id,
resources
.subtree(delete_path)
.filter(|r| !r.is_container)
.map(|r| r.document_id)
.collect::<Vec<_>>(),
book,
&mut batch,
)
.await
.caused_by(trc::location!())?;
DestroyArchive(book)
.delete_with_cards(
self,
access_token,
account_id,
document_id,
resources
.subtree(delete_path)
.filter(|r| !r.is_container)
.map(|r| r.document_id)
.collect::<Vec<_>>(),
&mut batch,
)
.await
.caused_by(trc::location!())?;
} else {
// Validate ACL
let addressbook_id = delete_resource.parent_id.unwrap();
@ -162,14 +161,16 @@ impl CardDeleteRequestHandler for Server {
.await?;
// Delete card
delete_card(
DestroyArchive(
card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?,
)
.delete(
access_token,
account_id,
document_id,
addressbook_id,
card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?,
&mut batch,
)
.caused_by(trc::location!())?;
@ -180,111 +181,3 @@ impl CardDeleteRequestHandler for Server {
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
}
pub(crate) async fn delete_address_book_and_cards(
server: &Server,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
children_ids: Vec<u32>,
book: Archive<&ArchivedAddressBook>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Process deletions
let addressbook_id = document_id;
for document_id in children_ids {
if let Some(card_) = server
.get_archive(account_id, Collection::ContactCard, document_id)
.await?
{
delete_card(
access_token,
account_id,
document_id,
addressbook_id,
card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?,
batch,
)?;
}
}
delete_address_book(access_token, account_id, document_id, book, batch)?;
Ok(())
}
pub(crate) fn delete_address_book(
access_token: &AccessToken,
account_id: u32,
document_id: u32,
book: Archive<&ArchivedAddressBook>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Delete addressbook
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(book),
)
.caused_by(trc::location!())?
.commit_point();
Ok(())
}
pub(crate) fn delete_card(
access_token: &AccessToken,
account_id: u32,
document_id: u32,
addressbook_id: u32,
card: Archive<&ArchivedContactCard>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
if let Some(delete_idx) = card
.inner
.names
.iter()
.position(|name| name.parent_id == addressbook_id)
{
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard);
if card.inner.names.len() > 1 {
// Unlink addressbook id from card
let mut new_card = card
.deserialize::<ContactCard>()
.caused_by(trc::location!())?;
new_card.names.swap_remove(delete_idx);
batch
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_tenant_id(access_token)
.with_current(card)
.with_changes(new_card),
)
.caused_by(trc::location!())?;
} else {
// Delete card
batch
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(card),
)
.caused_by(trc::location!())?;
}
batch.commit_point();
}
Ok(())
}

View file

@ -44,7 +44,7 @@ impl CardGetRequestHandler for Server {
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?;
let resource = resources

View file

@ -24,7 +24,7 @@ use crate::{
},
};
use super::{insert_addressbook, proppatch::CardPropPatchRequestHandler};
use super::proppatch::CardPropPatchRequestHandler;
pub(crate) trait CardMkColRequestHandler: Sync + Send {
fn handle_card_mkcol_request(
@ -54,7 +54,7 @@ impl CardMkColRequestHandler for Server {
if name.contains('/') || !access_token.is_member(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
} else if self
.fetch_dav_resources(account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?
.paths
@ -109,15 +109,8 @@ impl CardMkColRequestHandler for Server {
.assign_document_ids(account_id, Collection::AddressBook, 1)
.await
.caused_by(trc::location!())?;
insert_addressbook(
access_token,
book,
account_id,
document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
book.insert(access_token, account_id, document_id, &mut batch)
.caused_by(trc::location!())?;
self.commit_batch(batch).await.caused_by(trc::location!())?;
if let Some(prop_stat) = return_prop_stat {

View file

@ -4,24 +4,18 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use common::{DavResources, Server};
use dav_proto::schema::{
property::{CardDavProperty, DavProperty, WebDavProperty},
response::CardCondition,
};
use groupware::{
IDX_CARD_UID,
contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard},
};
use groupware::IDX_CARD_UID;
use hyper::StatusCode;
use jmap_proto::types::collection::Collection;
use store::{
query::Filter,
write::{Archive, BatchBuilder, now},
};
use store::query::Filter;
use trc::AddContext;
use crate::{DavError, DavErrorCondition, common::ExtractETag};
use crate::{DavError, DavErrorCondition};
pub mod copy_move;
pub mod delete;
@ -105,125 +99,12 @@ pub(crate) static CARD_ALL_PROPS: [DavProperty; 22] = [
DavProperty::CardDav(CardDavProperty::MaxResourceSize),
];
pub(crate) fn update_card(
access_token: &AccessToken,
card: Archive<&ArchivedContactCard>,
mut new_card: ContactCard,
account_id: u32,
document_id: u32,
with_etag: bool,
batch: &mut BatchBuilder,
) -> trc::Result<Option<String>> {
// Build card
new_card.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(card)
.with_changes(new_card)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(if with_etag { batch.etag() } else { None })
}
pub(crate) fn insert_card(
access_token: &AccessToken,
mut card: ContactCard,
account_id: u32,
document_id: u32,
with_etag: bool,
batch: &mut BatchBuilder,
) -> trc::Result<Option<String>> {
// Build card
let now = now() as i64;
card.modified = now;
card.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(card)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(if with_etag { batch.etag() } else { None })
}
pub(crate) fn insert_addressbook(
access_token: &AccessToken,
mut book: AddressBook,
account_id: u32,
document_id: u32,
with_etag: bool,
batch: &mut BatchBuilder,
) -> trc::Result<Option<String>> {
// Build card
let now = now() as i64;
book.modified = now;
book.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(book)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(if with_etag { batch.etag() } else { None })
}
pub(crate) fn update_addressbook(
access_token: &AccessToken,
book: Archive<&ArchivedAddressBook>,
mut new_book: AddressBook,
account_id: u32,
document_id: u32,
with_etag: bool,
batch: &mut BatchBuilder,
) -> trc::Result<Option<String>> {
// Build card
new_book.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(book)
.with_changes(new_book)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(if with_etag { batch.etag() } else { None })
}
pub(crate) async fn assert_is_unique_uid(
server: &Server,
resources: &DavResources,
account_id: u32,
addressbook_id: u32,
uid: Option<&str>,
base_uri: &str,
) -> crate::Result<()> {
if let Some(uid) = uid {
let hits = server
@ -243,7 +124,7 @@ pub(crate) async fn assert_is_unique_uid(
{
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(format!("{}/{}", base_uri, path.name).into()),
CardCondition::NoUidConflict(resources.format_resource(path).into()),
)));
}
}

View file

@ -27,14 +27,12 @@ use trc::AddContext;
use crate::{
DavError, DavMethod,
common::{
ETag,
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use super::{update_addressbook, update_card};
pub(crate) trait CardPropPatchRequestHandler: Sync + Send {
fn handle_card_proppatch_request(
&self,
@ -75,7 +73,7 @@ impl CardPropPatchRequestHandler for Server {
let uri = headers.uri;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?;
let resource = resource_
@ -172,16 +170,10 @@ impl CardPropPatchRequestHandler for Server {
}
if is_success {
update_addressbook(
access_token,
book,
new_book,
account_id,
document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
new_book
.update(access_token, book, account_id, document_id, &mut batch)
.caused_by(trc::location!())?
.etag()
} else {
book.etag().into()
}
@ -212,16 +204,10 @@ impl CardPropPatchRequestHandler for Server {
}
if is_success {
update_card(
access_token,
card,
new_card,
account_id,
document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
new_card
.update(access_token, card, account_id, document_id, &mut batch)
.caused_by(trc::location!())?
.etag()
} else {
card.etag().into()
}

View file

@ -50,7 +50,7 @@ impl CardQueryRequestHandler for Server {
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?;
let resource = resources
@ -82,13 +82,16 @@ impl CardQueryRequestHandler for Server {
// Obtain document ids in folder
let mut items = Vec::with_capacity(16);
let base_uri = headers.base_uri.unwrap_or_default();
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(base_uri, account_id, resource));
items.push(PropFindItem::new(
resources.format_resource(resource),
account_id,
resource,
));
}
}

View file

@ -20,14 +20,14 @@ use trc::AddContext;
use crate::{
DavError, DavErrorCondition, DavMethod,
common::{
ETag,
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use super::{assert_is_unique_uid, insert_card, update_card};
use super::assert_is_unique_uid;
pub(crate) trait CardUpdateRequestHandler: Sync + Send {
fn handle_card_update_request(
@ -54,7 +54,7 @@ impl CardUpdateRequestHandler for Server {
.into_owned_uri()?;
let account_id = resource.account_id;
let resources = self
.fetch_dav_resources(account_id, Collection::AddressBook)
.fetch_dav_resources(access_token, account_id, Collection::AddressBook)
.await
.caused_by(trc::location!())?;
let resource_name = resource
@ -169,9 +169,7 @@ impl CardUpdateRequestHandler for Server {
_ => {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(
headers.format_to_base_uri(resource_name).into(),
),
CardCondition::NoUidConflict(resources.format_resource(resource).into()),
)));
}
}
@ -185,16 +183,10 @@ impl CardUpdateRequestHandler for Server {
// Prepare write batch
let mut batch = BatchBuilder::new();
let etag = update_card(
access_token,
card,
new_card,
account_id,
document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = new_card
.update(access_token, card, account_id, document_id, &mut batch)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
@ -251,7 +243,6 @@ impl CardUpdateRequestHandler for Server {
account_id,
parent.document_id,
vcard.uid(),
headers.base_uri.unwrap_or_default(),
)
.await?;
@ -273,15 +264,10 @@ impl CardUpdateRequestHandler for Server {
.assign_document_ids(account_id, Collection::ContactCard, 1)
.await
.caused_by(trc::location!())?;
let etag = insert_card(
access_token,
card,
account_id,
document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = card
.insert(access_token, account_id, document_id, &mut batch)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))

View file

@ -5,7 +5,6 @@
*/
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use compact_str::format_compact;
use dav_proto::{
RequestHeaders,
schema::{
@ -25,14 +24,15 @@ use jmap_proto::types::{
collection::Collection,
value::{AclGrant, ArchivedAclGrant},
};
use percent_encoding::NON_ALPHANUMERIC;
use rkyv::vec::ArchivedVec;
use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder};
use trc::AddContext;
use utils::map::bitmap::Bitmap;
use crate::{
DavError, DavErrorCondition, DavResource, card::update_addressbook,
common::uri::DavUriResource, file::update_file_node, principal::propfind::PrincipalPropFind,
DavError, DavErrorCondition, DavResourceName, common::uri::DavUriResource,
principal::propfind::PrincipalPropFind,
};
use super::ArchivedResource;
@ -108,7 +108,7 @@ impl DavAclHandler for Server {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let resources = self
.fetch_dav_resources(account_id, collection)
.fetch_dav_resources(access_token, account_id, collection)
.await
.caused_by(trc::location!())?;
let resource = resource_
@ -152,31 +152,29 @@ impl DavAclHandler for Server {
.deserialize::<AddressBook>()
.caused_by(trc::location!())?;
new_book.acls = grants;
update_addressbook(
access_token,
book,
new_book,
account_id,
resource.document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_book
.update(
access_token,
book,
account_id,
resource.document_id,
&mut batch,
)
.caused_by(trc::location!())?;
}
ArchivedResource::FileNode(node) => {
let mut new_node =
node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.acls = grants;
update_file_node(
access_token,
node,
new_node,
account_id,
resource.document_id,
false,
&mut batch,
)
.caused_by(trc::location!())?;
new_node
.update(
access_token,
node,
account_id,
resource.document_id,
&mut batch,
)
.caused_by(trc::location!())?;
}
_ => unreachable!(),
}
@ -198,7 +196,7 @@ impl DavAclHandler for Server {
.await
.and_then(|uri| uri.into_owned_uri())?;
let uri = self
.map_uri_resource(uri)
.map_uri_resource(access_token, uri)
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
@ -497,9 +495,12 @@ impl DavAclHandler for Server {
aces.push(Ace::new(
Principal::Href(Href(format!(
"{}/{}",
DavResource::Principal.base_path(),
grant_account_name,
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(
&grant_account_name,
NON_ALPHANUMERIC
),
))),
GrantDeny::grant(privileges),
));
@ -515,6 +516,7 @@ pub(crate) trait Privileges {
&self,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
is_calendar: bool,
) -> Vec<Privilege>;
}
@ -523,21 +525,10 @@ impl Privileges for AccessToken {
&self,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
is_calendar: bool,
) -> Vec<Privilege> {
if self.is_member(account_id) {
vec![
Privilege::Read,
Privilege::Write,
Privilege::WriteProperties,
Privilege::WriteContent,
Privilege::Unlock,
Privilege::ReadAcl,
Privilege::ReadCurrentUserPrivilegeSet,
Privilege::WriteAcl,
Privilege::Bind,
Privilege::Unbind,
Privilege::ReadFreeBusy,
]
Privilege::all(is_calendar)
} else {
let mut acls = AHashSet::with_capacity(16);
for grant in grants.effective_acl(self) {

View file

@ -122,6 +122,7 @@ impl LockRequestHandler for Server {
..Default::default()
}];
let mut base_path = None;
let is_lock_request = !matches!(lock_info, LockRequest::Unlock);
let if_lock_token = headers
.if_
@ -170,7 +171,9 @@ impl LockRequestHandler for Server {
|| lock_item.depth_infinity && resource_path.len() > lock_path.len()
|| is_infinity && lock_path.len() > resource_path.len())
{
failed_locks.push(headers.format_to_base_uri(lock_path).into());
let base_path =
base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
failed_locks.push(format!("{base_path}/{lock_path}").into());
}
}
@ -266,7 +269,8 @@ impl LockRequestHandler for Server {
lock_item.exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive);
}
let active_lock = lock_item.to_active_lock(headers.format_to_base_uri(resource_path));
let base_path = base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
let active_lock = lock_item.to_active_lock(format!("{base_path}/{resource_path}"));
HttpResponse::new(StatusCode::CREATED)
.with_lock_token(&active_lock.lock_token.as_ref().unwrap().0)
@ -364,6 +368,8 @@ impl LockRequestHandler for Server {
method,
DavMethod::GET | DavMethod::HEAD | DavMethod::LOCK | DavMethod::UNLOCK
) {
let mut base_path = None;
'outer: for (pos, resource) in resources.iter().enumerate() {
if pos == 0 && matches!(method, DavMethod::COPY) {
continue;
@ -382,7 +388,11 @@ impl LockRequestHandler for Server {
}) {
break 'outer;
} else {
failed_locks.push(headers.format_to_base_uri(lock_path).into());
let base_path = base_path.get_or_insert_with(|| {
headers.base_uri()
.unwrap_or_default()
});
failed_locks.push(format!("{base_path}/{lock_path}").into());
}
}
@ -475,11 +485,14 @@ impl LockRequestHandler for Server {
if needs_etag && resource_state.etag.is_none() {
if resource_state.document_id.is_none() {
resource_state.document_id = self
.map_uri_resource(UriResource {
collection: resource_state.collection,
account_id: resource_state.account_id,
resource: resource_state.path.into(),
})
.map_uri_resource(
access_token,
UriResource {
collection: resource_state.collection,
account_id: resource_state.account_id,
resource: resource_state.path.into(),
},
)
.await
.caused_by(trc::location!())?
.map(|uri| uri.resource)

View file

@ -38,10 +38,9 @@ pub mod lock;
pub mod propfind;
pub mod uri;
#[derive(Default)]
#[derive(Default, Debug)]
pub(crate) struct DavQuery<'x> {
pub resource: DavQueryResource<'x>,
pub base_uri: &'x str,
pub propfind: PropFind,
pub from_change_id: Option<u64>,
pub depth: usize,
@ -50,7 +49,7 @@ pub(crate) struct DavQuery<'x> {
pub depth_no_root: bool,
}
#[derive(Default)]
#[derive(Default, Debug)]
pub(crate) enum DavQueryResource<'x> {
Uri(OwnedUri<'x>),
Multiget {
@ -70,6 +69,7 @@ pub(crate) type AddressbookFilter = Vec<Filter<(), VCardPropertyWithGroup, VCard
pub(crate) type CalendarFilter =
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>;
#[derive(Debug)]
pub(crate) enum DavQueryFilter {
Addressbook(AddressbookFilter),
Calendar {
@ -136,7 +136,6 @@ impl<'x> DavQuery<'x> {
Self {
resource: DavQueryResource::Uri(resource),
propfind,
base_uri: headers.base_uri.unwrap_or_default(),
depth: match headers.depth {
Depth::Zero => 0,
_ => 1,
@ -158,7 +157,6 @@ impl<'x> DavQuery<'x> {
parent_collection: collection,
},
propfind: multiget.properties,
base_uri: headers.base_uri.unwrap_or_default(),
ret: headers.ret,
depth_no_root: headers.depth_no_root,
..Default::default()
@ -177,7 +175,6 @@ impl<'x> DavQuery<'x> {
items,
},
propfind: query.properties,
base_uri: headers.base_uri.unwrap_or_default(),
limit: query.limit,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
@ -200,7 +197,6 @@ impl<'x> DavQuery<'x> {
items,
},
propfind: query.properties,
base_uri: headers.base_uri.unwrap_or_default(),
ret: headers.ret,
depth_no_root: headers.depth_no_root,
..Default::default()
@ -215,7 +211,6 @@ impl<'x> DavQuery<'x> {
Self {
resource: DavQueryResource::Uri(resource),
propfind: changes.properties,
base_uri: headers.base_uri.unwrap_or_default(),
from_change_id: changes
.sync_token
.as_deref()
@ -227,14 +222,9 @@ impl<'x> DavQuery<'x> {
limit: changes.limit,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
..Default::default()
}
}
pub fn format_to_base_uri(&self, path: &str) -> String {
format!("{}/{}", self.base_uri, path)
}
pub fn is_minimal(&self) -> bool {
self.ret == Return::Minimal
}

View file

@ -13,11 +13,13 @@ use common::{
};
use dav_proto::{
Depth, RequestHeaders,
parser::header::dav_base_uri,
schema::{
Collation,
Collation, Namespace,
property::{
ActiveLock, CardDavProperty, DavProperty, DavValue, Privilege, ResourceType,
Rfc1123DateTime, SupportedCollation, SupportedLock, WebDavProperty,
ActiveLock, CardDavProperty, DavProperty, DavValue, PrincipalProperty, Privilege,
ReportSet, ResourceType, Rfc1123DateTime, SupportedCollation, SupportedLock,
WebDavProperty,
},
request::{DavPropertyValue, PropFind},
response::{
@ -26,14 +28,12 @@ use dav_proto::{
},
},
};
use directory::{
Type,
backend::internal::{PrincipalField, manage::ManageDirectory},
};
use groupware::hierarchy::DavHierarchy;
use directory::{Type, backend::internal::manage::ManageDirectory};
use groupware::{DavResourceName, hierarchy::DavHierarchy};
use http_proto::HttpResponse;
use hyper::StatusCode;
use jmap_proto::types::{acl::Acl, collection::Collection};
use percent_encoding::NON_ALPHANUMERIC;
use store::{
ahash::AHashMap,
query::log::Query,
@ -97,6 +97,7 @@ pub(crate) struct PropFindAccountQuota {
pub available: u64,
}
#[derive(Debug)]
pub(crate) struct PropFindItem {
pub name: String,
pub account_id: u32,
@ -155,7 +156,11 @@ impl PropFindRequestHandler for Server {
if let Some(resource) = resource.resource {
response.add_response(Response::new_status(
[headers.format_to_base_uri(resource)],
[format!(
"{}/{}",
headers.base_uri().unwrap_or_default(),
resource
)],
StatusCode::NOT_FOUND,
));
} else {
@ -179,40 +184,102 @@ impl PropFindRequestHandler for Server {
// Add container info
if !headers.depth_no_root {
let mut prop_stat = match &request {
PropFind::PropName | PropFind::AllProp(_) => {
vec![
DavPropertyValue::empty(DavProperty::WebDav(
WebDavProperty::ResourceType,
)),
DavPropertyValue::empty(DavProperty::WebDav(
WebDavProperty::CurrentUserPrincipal,
)),
]
}
PropFind::Prop(items) => {
items.iter().cloned().map(DavPropertyValue::empty).collect()
let properties = match &request {
PropFind::PropName => {
response.add_response(Response::new_propstat(
resource.collection_path(),
vec![PropStat::new_list(vec![
DavPropertyValue::empty(DavProperty::WebDav(
WebDavProperty::ResourceType,
)),
DavPropertyValue::empty(DavProperty::WebDav(
WebDavProperty::CurrentUserPrincipal,
)),
DavPropertyValue::empty(DavProperty::WebDav(
WebDavProperty::SupportedReportSet,
)),
])],
));
&[]
}
PropFind::AllProp(_) => [
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
]
.as_slice(),
PropFind::Prop(items) => items,
};
if !matches!(request, PropFind::PropName) {
for prop in &mut prop_stat {
match &prop.property {
let mut fields = Vec::with_capacity(properties.len());
let mut fields_not_found = Vec::new();
for prop in properties {
match &prop {
DavProperty::WebDav(WebDavProperty::ResourceType) => {
prop.value = vec![ResourceType::Collection].into();
fields.push(DavPropertyValue::new(
prop.clone(),
vec![ResourceType::Collection],
));
}
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal) => {
prop.value = vec![access_token.current_user_principal()].into();
fields.push(DavPropertyValue::new(
prop.clone(),
vec![access_token.current_user_principal()],
));
}
DavProperty::Principal(PrincipalProperty::AddressbookHomeSet) => {
fields.push(DavPropertyValue::new(
prop.clone(),
vec![Href(format!(
"{}/{}/",
DavResourceName::Card.base_path(),
percent_encoding::utf8_percent_encode(
&access_token.name,
NON_ALPHANUMERIC
),
))],
));
response.set_namespace(Namespace::CardDav);
}
DavProperty::WebDav(WebDavProperty::SupportedReportSet) => {
let reports = match resource.collection {
Collection::Principal => ReportSet::principal(),
Collection::Calendar | Collection::CalendarEvent => {
ReportSet::calendar()
}
Collection::AddressBook | Collection::ContactCard => {
ReportSet::addressbook()
}
_ => ReportSet::file(),
};
fields.push(DavPropertyValue::new(prop.clone(), reports));
}
_ => {
fields_not_found.push(DavPropertyValue::empty(prop.clone()));
}
_ => (),
}
}
}
response.add_response(Response::new_propstat(
resource.base_path(),
vec![PropStat::new_list(prop_stat)],
));
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(
resource.collection_path(),
prop_stat,
));
}
}
if return_children {
@ -259,19 +326,23 @@ impl PropFindRequestHandler for Server {
let mut data = PropFindData::new();
let collection_container;
let collection_children;
let mut ctag = None;
let mut paths;
let mut query_filter = None;
//let c = println!("handling DAV query {query:#?}");
match std::mem::take(&mut query.resource) {
DavQueryResource::Uri(resource) => {
let account_id = resource.account_id;
collection_container = resource.collection;
collection_children = collection_container.child_collection().unwrap();
let resources = self
.fetch_dav_resources(account_id, collection_container)
.fetch_dav_resources(access_token, account_id, collection_container)
.await
.caused_by(trc::location!())?;
response.set_namespace(collection_container.namespace());
ctag = Some(resources.modseq.unwrap_or_default());
// Obtain document ids
let mut document_ids = if !access_token.is_member(account_id) {
@ -335,7 +406,9 @@ impl PropFindRequestHandler for Server {
.as_ref()
.is_none_or(|d| d.contains(item.document_id))
})
.map(|item| PropFindItem::new(query.base_uri, account_id, item))
.map(|item| {
PropFindItem::new(resources.format_resource(item), account_id, item)
})
.collect::<Vec<_>>()
} else {
if !query.depth_no_root || query.from_change_id.is_none() {
@ -353,6 +426,7 @@ impl PropFindRequestHandler for Server {
.with_xml_body(response.to_string()));
}
}
resources
.tree_with_depth(query.depth - 1)
.filter(|item| {
@ -360,15 +434,19 @@ impl PropFindRequestHandler for Server {
.as_ref()
.is_none_or(|d| d.contains(item.document_id))
})
.map(|item| PropFindItem::new(query.base_uri, account_id, item))
.map(|item| {
PropFindItem::new(resources.format_resource(item), account_id, item)
})
.collect::<Vec<_>>()
};
if paths.is_empty() && query.from_change_id.is_none() {
response.add_response(Response::new_status(
[query.format_to_base_uri(resource.resource.unwrap_or_default())],
StatusCode::NOT_FOUND,
));
if let Some(resource) = resource.resource {
response.add_response(Response::new_status(
[resources.format_item(resource)],
StatusCode::NOT_FOUND,
));
}
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(response.to_string()));
@ -409,7 +487,7 @@ impl PropFindRequestHandler for Server {
resources.clone()
} else {
let resources = self
.fetch_dav_resources(account_id, collection_container)
.fetch_dav_resources(access_token, account_id, collection_container)
.await
.caused_by(trc::location!())?;
let document_ids = Arc::new(if !access_token.is_member(account_id) {
@ -430,6 +508,15 @@ impl PropFindRequestHandler for Server {
(resources, document_ids)
};
let c = println!(
"resources: {:?} resource: {resource:?}",
resources
.paths
.iter()
.map(|r| r.name.to_string())
.collect::<Vec<_>>()
);
if let Some(resource) = resource
.resource
.and_then(|name| resources.paths.by_name(name))
@ -440,7 +527,11 @@ impl PropFindRequestHandler for Server {
.as_ref()
.is_none_or(|docs| docs.contains(resource.document_id))
{
paths.push(PropFindItem::new(query.base_uri, account_id, resource));
paths.push(PropFindItem::new(
resources.format_resource(resource),
account_id,
resource,
));
} else {
response.add_response(
Response::new_status([item], StatusCode::FORBIDDEN)
@ -484,7 +575,7 @@ impl PropFindRequestHandler for Server {
}
let mut is_all_prop = false;
let todo = "prop lists";
let todo = "prop lists for calendar";
let properties = match &query.propfind {
PropFind::PropName => {
let (container_props, children_props) = match collection_container {
@ -636,6 +727,25 @@ impl PropFindRequestHandler for Server {
DavValue::String(archive_.etag()),
));
}
WebDavProperty::GetCTag => {
if item.is_container {
let ctag = if let Some(ctag) = ctag {
ctag
} else {
self.store()
.get_last_change_id(account_id, collection)
.await?
.unwrap_or_default()
};
fields.push(DavPropertyValue::new(
property.clone(),
DavValue::String(format!("\"{ctag}\"")),
));
} else {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
response.set_namespace(Namespace::CalendarServer);
}
WebDavProperty::GetLastModified => {
fields.push(DavPropertyValue::new(
property.clone(),
@ -651,7 +761,7 @@ impl PropFindRequestHandler for Server {
}
WebDavProperty::LockDiscovery => {
if let Some(locks) = data
.locks(self, account_id, collection_container, &query, &item)
.locks(self, account_id, collection_container, &item)
.await
.caused_by(trc::location!())?
{
@ -793,7 +903,11 @@ impl PropFindRequestHandler for Server {
if let Some(acls) = archive.acls() {
fields.push(DavPropertyValue::new(
property.clone(),
access_token.current_privilege_set(account_id, acls),
access_token.current_privilege_set(
account_id,
acls,
collection_container == Collection::Calendar,
),
));
} else if !is_all_prop {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
@ -825,7 +939,9 @@ impl PropFindRequestHandler for Server {
WebDavProperty::PrincipalCollectionSet => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(crate::DavResource::Principal.base_path().to_string())],
vec![Href(
DavResourceName::Principal.collection_path().to_string(),
)],
));
}
},
@ -862,9 +978,14 @@ impl PropFindRequestHandler for Server {
fields.push(DavPropertyValue::new(
property.clone(),
DavValue::Collations(List(vec![
SupportedCollation(Collation::AsciiCasemap),
SupportedCollation(Collation::UnicodeCasemap),
SupportedCollation(Collation::Octet),
SupportedCollation {
collation: Collation::AsciiCasemap,
namespace: Namespace::CardDav,
},
SupportedCollation {
collation: Collation::UnicodeCasemap,
namespace: Namespace::CardDav,
},
])),
));
}
@ -970,7 +1091,7 @@ impl PropFindRequestHandler for Server {
} else if let Some(tenant) = resource_token.tenant.filter(|t| t.quota > 0) {
tenant.quota
} else {
u64::MAX
u32::MAX as u64
};
let used = self
.get_used_quota(account_id)
@ -985,9 +1106,9 @@ impl PropFindRequestHandler for Server {
}
impl PropFindItem {
pub fn new(base_uri: &str, account_id: u32, resource: &DavResource) -> Self {
pub fn new(name: String, account_id: u32, resource: &DavResource) -> Self {
Self {
name: format!("{}{}", base_uri, resource.name),
name,
account_id,
document_id: resource.document_id,
is_container: resource.is_container,
@ -1062,7 +1183,6 @@ impl PropFindData {
server: &Server,
account_id: u32,
collection_container: Collection,
query: &DavQuery<'_>,
item: &PropFindItem,
) -> trc::Result<Option<Vec<ActiveLock>>> {
let data = self.accounts.entry(account_id).or_default();
@ -1081,11 +1201,12 @@ impl PropFindData {
}
if let Some(lock_data) = &data.locks {
let base_uri = dav_base_uri(&item.name).unwrap_or_default();
lock_data.unarchive::<LockData>().map(|locks| {
locks
.find_locks(&item.name.strip_prefix(query.base_uri).unwrap()[1..], false)
.find_locks(&item.name.strip_prefix(base_uri).unwrap()[1..], false)
.iter()
.map(|(path, lock)| lock.to_active_lock(query.format_to_base_uri(path)))
.map(|(path, lock)| lock.to_active_lock(format!("{base_uri}/{path}")))
.collect::<Vec<_>>()
.into()
})

View file

@ -15,7 +15,7 @@ use hyper::StatusCode;
use jmap_proto::types::collection::Collection;
use trc::AddContext;
use crate::{DavError, DavResource};
use crate::{DavError, DavResourceName};
#[derive(Debug)]
pub(crate) struct UriResource<A, R> {
@ -42,6 +42,7 @@ pub(crate) trait DavUriResource: Sync + Send {
fn map_uri_resource(
&self,
access_token: &AccessToken,
uri: OwnedUri<'_>,
) -> impl Future<Output = trc::Result<Option<DocumentUri>>> + Send;
}
@ -63,7 +64,7 @@ impl DavUriResource for Server {
let mut resource = UriResource {
collection: uri_parts
.next()
.and_then(DavResource::parse)
.and_then(DavResourceName::parse)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
.into(),
account_id: None,
@ -103,10 +104,14 @@ impl DavUriResource for Server {
Ok(resource)
}
async fn map_uri_resource(&self, uri: OwnedUri<'_>) -> trc::Result<Option<DocumentUri>> {
async fn map_uri_resource(
&self,
access_token: &AccessToken,
uri: OwnedUri<'_>,
) -> trc::Result<Option<DocumentUri>> {
if let Some(resource) = uri.resource {
if let Some(resource) = self
.fetch_dav_resources(uri.account_id, uri.collection)
.fetch_dav_resources(access_token, uri.account_id, uri.collection)
.await
.caused_by(trc::location!())?
.paths
@ -159,8 +164,8 @@ impl OwnedUri<'_> {
}
impl<A, R> UriResource<A, R> {
pub fn base_path(&self) -> &'static str {
DavResource::from(self.collection).base_path()
pub fn collection_path(&self) -> &'static str {
DavResourceName::from(self.collection).collection_path()
}
}

View file

@ -8,7 +8,7 @@ use std::sync::Arc;
use common::{DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use dav_proto::{Depth, RequestHeaders};
use groupware::{file::FileNode, hierarchy::DavHierarchy};
use groupware::{DestroyArchive, file::FileNode, hierarchy::DavHierarchy};
use http_proto::HttpResponse;
use hyper::StatusCode;
use jmap_proto::types::{acl::Acl, collection::Collection};
@ -22,14 +22,15 @@ use utils::map::bitmap::Bitmap;
use crate::{
DavError, DavMethod,
common::{
ExtractETag,
acl::DavAclHandler,
lock::{LockRequestHandler, ResourceState},
uri::{DavUriResource, UriResource},
},
file::{DavFileResource, FileItemId, insert_file_node, update_file_node},
file::{DavFileResource, FileItemId},
};
use super::{FromDavResource, delete::delete_files, delete_file_node};
use super::FromDavResource;
pub(crate) trait FileCopyMoveRequestHandler: Sync + Send {
fn handle_file_copy_move_request(
@ -54,7 +55,7 @@ impl FileCopyMoveRequestHandler for Server {
.into_owned_uri()?;
let from_account_id = from_resource_.account_id;
let from_files = self
.fetch_dav_resources(from_account_id, Collection::FileNode)
.fetch_dav_resources(access_token, from_account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let from_resource = from_files.map_resource::<FileItemId>(&from_resource_)?;
@ -100,7 +101,7 @@ impl FileCopyMoveRequestHandler for Server {
let to_files = if to_account_id == from_account_id {
from_files.clone()
} else {
self.fetch_dav_resources(to_account_id, Collection::FileNode)
self.fetch_dav_resources(access_token, to_account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?
};
@ -234,7 +235,8 @@ impl FileCopyMoveRequestHandler for Server {
ids.sort_unstable_by(|a, b| b.hierarchy_sequence.cmp(&a.hierarchy_sequence));
let mut sorted_ids = Vec::with_capacity(ids.len());
sorted_ids.extend(ids.into_iter().map(|a| a.document_id));
delete_files(self, access_token, destination.account_id, sorted_ids)
DestroyArchive(sorted_ids)
.delete(self, access_token, destination.account_id)
.await
.caused_by(trc::location!())?;
}
@ -340,22 +342,22 @@ async fn move_container(
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize().caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = update_file_node(
access_token,
node,
new_node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = new_node
.update(
access_token,
node,
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await
@ -536,7 +538,9 @@ async fn overwrite_and_delete_item(
let source_node_ = source_node__
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut source_node = source_node_.deserialize().caused_by(trc::location!())?;
let mut source_node = source_node_
.deserialize::<FileNode>()
.caused_by(trc::location!())?;
source_node.name = if let Some(new_name) = destination.new_name {
new_name
} else {
@ -545,25 +549,19 @@ async fn overwrite_and_delete_item(
source_node.parent_id = dest_node.inner.parent_id.into();
let mut batch = BatchBuilder::new();
let etag = update_file_node(
access_token,
dest_node,
source_node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
delete_file_node(
access_token,
source_node_,
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = source_node
.update(
access_token,
dest_node,
to_account_id,
to_document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
DestroyArchive(source_node_)
.delete(access_token, from_account_id, from_document_id, &mut batch)
.caused_by(trc::location!())?;
server
.commit_batch(batch)
.await
@ -610,16 +608,16 @@ async fn overwrite_item(
};
source_node.parent_id = dest_node.inner.parent_id.into();
let mut batch = BatchBuilder::new();
let etag = update_file_node(
access_token,
dest_node,
source_node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = source_node
.update(
access_token,
dest_node,
to_account_id,
to_document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await
@ -648,7 +646,7 @@ async fn move_item(
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize().caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
@ -657,16 +655,16 @@ async fn move_item(
let mut batch = BatchBuilder::new();
let etag = if from_account_id == to_account_id {
// Destination is in the same account: just update the parent id
update_file_node(
access_token,
node,
new_node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
new_node
.update(
access_token,
node,
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
// Destination is in a different account: insert a new node, then delete the old one
let to_document_id = server
@ -674,23 +672,13 @@ async fn move_item(
.assign_document_ids(to_account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let etag = insert_file_node(
access_token,
new_node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
delete_file_node(
access_token,
node,
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = new_node
.insert(access_token, to_account_id, to_document_id, &mut batch)
.caused_by(trc::location!())?
.etag();
DestroyArchive(node)
.delete(access_token, from_account_id, from_document_id, &mut batch)
.caused_by(trc::location!())?;
etag
};
server
@ -730,15 +718,10 @@ async fn copy_item(
.assign_document_ids(to_account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let etag = insert_file_node(
access_token,
node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = node
.insert(access_token, to_account_id, to_document_id, &mut batch)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await
@ -765,21 +748,21 @@ async fn rename_item(
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize().caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = update_file_node(
access_token,
node,
new_node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = new_node
.update(
access_token,
node,
from_account_id,
from_document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await

View file

@ -4,13 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use common::{Server, auth::AccessToken};
use dav_proto::RequestHeaders;
use groupware::{file::FileNode, hierarchy::DavHierarchy};
use groupware::{DestroyArchive, hierarchy::DavHierarchy};
use http_proto::HttpResponse;
use hyper::StatusCode;
use jmap_proto::types::{acl::Acl, collection::Collection};
use store::write::BatchBuilder;
use trc::AddContext;
use crate::{
@ -46,7 +45,7 @@ impl FileDeleteRequestHandler for Server {
.filter(|r| !r.is_empty())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let files = self
.fetch_dav_resources(account_id, Collection::FileNode)
.fetch_dav_resources(access_token, account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
@ -91,51 +90,10 @@ impl FileDeleteRequestHandler for Server {
)
.await?;
delete_files(self, access_token, account_id, sorted_ids).await?;
DestroyArchive(sorted_ids)
.delete(self, access_token, account_id)
.await?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
}
pub(crate) async fn delete_files(
server: &Server,
access_token: &AccessToken,
account_id: u32,
ids: Vec<u32>,
) -> trc::Result<()> {
// Process deletions
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode);
for document_id in ids {
if let Some(node) = server
.get_archive(account_id, Collection::FileNode, document_id)
.await?
{
// Delete record
batch
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(
node.to_unarchived::<FileNode>()
.caused_by(trc::location!())?,
),
)
.caused_by(trc::location!())?
.commit_point();
}
}
// Write changes
if !batch.is_empty() {
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
}
Ok(())
}

View file

@ -45,7 +45,7 @@ impl FileGetRequestHandler for Server {
.into_owned_uri()?;
let account_id = resource_.account_id;
let files = self
.fetch_dav_resources(account_id, Collection::FileNode)
.fetch_dav_resources(access_token, account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let resource = files.map_resource(&resource_)?;

View file

@ -51,7 +51,7 @@ impl FileMkColRequestHandler for Server {
.into_owned_uri()?;
let account_id = resource_.account_id;
let files = self
.fetch_dav_resources(account_id, Collection::FileNode)
.fetch_dav_resources(access_token, account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let resource = files.map_parent_resource(&resource_)?;

View file

@ -4,19 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{DavResource, DavResources, auth::AccessToken, storage::index::ObjectIndexBuilder};
use common::{DavResource, DavResources};
use dav_proto::schema::property::{DavProperty, WebDavProperty};
use groupware::file::{ArchivedFileNode, FileNode};
use hyper::StatusCode;
use jmap_proto::types::collection::Collection;
use store::write::{Archive, BatchBuilder, now};
use crate::{
DavError,
common::{
ExtractETag,
uri::{OwnedUri, UriResource},
},
common::uri::{OwnedUri, UriResource},
};
pub mod copy_move;
@ -178,78 +172,3 @@ impl FromDavResource for FileItemId {
}
}
}
pub(crate) fn update_file_node(
access_token: &AccessToken,
node: Archive<&ArchivedFileNode>,
mut new_node: FileNode,
account_id: u32,
document_id: u32,
with_etag: bool,
batch: &mut BatchBuilder,
) -> trc::Result<Option<String>> {
// Build node
new_node.modified = now() as i64;
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(node)
.with_changes(new_node)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(if with_etag { batch.etag() } else { None })
}
pub(crate) fn insert_file_node(
access_token: &AccessToken,
mut node: FileNode,
account_id: u32,
document_id: u32,
with_etag: bool,
batch: &mut BatchBuilder,
) -> trc::Result<Option<String>> {
// Build node
let now = now() as i64;
node.modified = now;
node.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(node)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(if with_etag { batch.etag() } else { None })
}
pub(crate) fn delete_file_node(
access_token: &AccessToken,
node: Archive<&ArchivedFileNode>,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_current(node)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(())
}

View file

@ -23,15 +23,13 @@ use trc::AddContext;
use crate::{
DavError, DavMethod,
common::{
ETag,
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use super::update_file_node;
pub(crate) trait FilePropPatchRequestHandler: Sync + Send {
fn handle_file_proppatch_request(
&self,
@ -64,7 +62,7 @@ impl FilePropPatchRequestHandler for Server {
let uri = headers.uri;
let account_id = resource_.account_id;
let files = self
.fetch_dav_resources(account_id, Collection::FileNode)
.fetch_dav_resources(access_token, account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let resource = files.map_resource(&resource_)?;
@ -112,7 +110,7 @@ impl FilePropPatchRequestHandler for Server {
.await?;
// Deserialize
let mut new_node = node.deserialize().caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
// Remove properties
let mut items = Vec::with_capacity(request.remove.len() + request.set.len());
@ -134,16 +132,16 @@ impl FilePropPatchRequestHandler for Server {
let etag = if is_success {
let mut batch = BatchBuilder::new();
let etag = update_file_node(
access_token,
node,
new_node,
account_id,
resource.resource,
true,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = new_node
.update(
access_token,
node,
account_id,
resource.resource,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
etag
} else {

View file

@ -55,7 +55,7 @@ impl FileUpdateRequestHandler for Server {
.into_owned_uri()?;
let account_id = resource.account_id;
let files = self
.fetch_dav_resources(account_id, Collection::FileNode)
.fetch_dav_resources(access_token, account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let resource_name = resource

View file

@ -12,20 +12,11 @@ pub mod principal;
pub mod request;
use dav_proto::schema::response::Condition;
use http_proto::HttpResponse;
use groupware::DavResourceName;
use hyper::{Method, StatusCode};
use jmap_proto::types::collection::Collection;
pub(crate) type Result<T> = std::result::Result<T, DavError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DavResource {
Card,
Cal,
File,
Principal,
}
#[derive(Debug, Clone, Copy)]
pub enum DavMethod {
GET,
@ -82,86 +73,6 @@ impl DavErrorCondition {
}
}
impl From<DavResource> for Collection {
fn from(value: DavResource) -> Self {
match value {
DavResource::Card => Collection::AddressBook,
DavResource::Cal => Collection::Calendar,
DavResource::File => Collection::FileNode,
DavResource::Principal => Collection::Principal,
}
}
}
impl From<Collection> for DavResource {
fn from(value: Collection) -> Self {
match value {
Collection::AddressBook => DavResource::Card,
Collection::Calendar => DavResource::Cal,
Collection::FileNode => DavResource::File,
Collection::Principal => DavResource::Principal,
_ => unreachable!(),
}
}
}
impl DavResource {
pub fn parse(service: &str) -> Option<Self> {
hashify::tiny_map!(service.as_bytes(),
"card" => DavResource::Card,
"cal" => DavResource::Cal,
"file" => DavResource::File,
"pal" => DavResource::Principal,
)
}
pub fn base_path(&self) -> &'static str {
match self {
DavResource::Card => "/dav/card",
DavResource::Cal => "/dav/cal",
DavResource::File => "/dav/file",
DavResource::Principal => "/dav/pal",
}
}
pub fn into_options_response(self, depth: usize) -> HttpResponse {
/*
Depth:
0 -> /dav/{resource_type}
1 -> /dav/{resource_type}/{account_id}
2 -> /dav/{resource_type}/{account_id}/{resource}
*/
let dav = match self {
DavResource::Cal => "1, 2, 3, access-control, extended-mkcol, calendar-access",
DavResource::Card => "1, 2, 3, access-control, extended-mkcol, addressbook",
DavResource::File => "1, 2, 3, access-control, extended-mkcol",
DavResource::Principal => "1, 2, 3, access-control",
};
let allow = match depth {
0 => "OPTIONS, PROPFIND, REPORT",
1 => {
if self != DavResource::Principal {
"OPTIONS, PROPFIND, MKCOL, REPORT"
} else {
"OPTIONS, PROPFIND, REPORT"
}
}
_ => {
if self != DavResource::Principal {
"OPTIONS, GET, HEAD, POST, PUT, DELETE, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL"
} else {
"OPTIONS, PROPFIND, REPORT"
}
}
};
HttpResponse::new(StatusCode::OK)
.with_header("DAV", dav)
.with_header("Allow", allow)
}
}
impl DavMethod {
pub fn parse(method: &Method) -> Option<Self> {
match *method {

View file

@ -52,7 +52,6 @@ impl PrincipalMatching for Server {
access_token,
DavQuery {
resource: DavQueryResource::Uri(resource),
base_uri: headers.base_uri.unwrap_or_default(),
propfind: PropFind::Prop(request.properties),
depth: usize::MAX,
ret: headers.ret,

View file

@ -8,7 +8,7 @@ use common::auth::AccessToken;
use dav_proto::schema::response::Href;
use percent_encoding::NON_ALPHANUMERIC;
use crate::DavResource;
use crate::DavResourceName;
pub mod matching;
pub mod propfind;
@ -21,8 +21,8 @@ pub trait CurrentUserPrincipal {
impl CurrentUserPrincipal for AccessToken {
fn current_user_principal(&self) -> Href {
Href(format!(
"{}/{}",
DavResource::Principal.base_path(),
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(&self.name, NON_ALPHANUMERIC)
))
}

View file

@ -7,10 +7,11 @@
use std::borrow::Cow;
use common::{Server, auth::AccessToken};
use compact_str::format_compact;
use dav_proto::schema::{
Namespace,
property::{DavProperty, PrincipalProperty, ReportSet, ResourceType, WebDavProperty},
property::{
DavProperty, PrincipalProperty, Privilege, ReportSet, ResourceType, WebDavProperty,
},
request::{DavPropertyValue, PropFind},
response::{Href, MultiStatus, PropStat, Response},
};
@ -21,7 +22,7 @@ use percent_encoding::NON_ALPHANUMERIC;
use trc::AddContext;
use crate::{
DavResource,
DavResourceName,
common::{propfind::PropFindRequestHandler, uri::Urn},
};
@ -83,7 +84,7 @@ impl PrincipalPropFind for Server {
Collection::Principal => true,
_ => false,
};
let base_path = DavResource::from(collection).base_path();
let base_path = DavResourceName::from(collection).base_path();
let needs_quota = properties.iter().any(|property| {
matches!(
property,
@ -142,28 +143,25 @@ impl PrincipalPropFind for Server {
}
WebDavProperty::ResourceType => {
let resource_type = if !is_principal {
ResourceType::Collection
vec![ResourceType::Collection]
} else {
ResourceType::Principal
vec![ResourceType::Principal, ResourceType::Collection]
};
fields
.push(DavPropertyValue::new(property.clone(), vec![resource_type]));
fields.push(DavPropertyValue::new(property.clone(), resource_type));
}
WebDavProperty::SupportedReportSet => {
let reports = if !is_principal {
vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
]
} else {
vec![
ReportSet::PrincipalPropertySearch,
ReportSet::PrincipalSearchPropertySet,
ReportSet::PrincipalMatch,
]
let reports = match collection {
Collection::Principal => ReportSet::principal(),
Collection::Calendar | Collection::CalendarEvent => {
ReportSet::calendar()
}
Collection::AddressBook | Collection::ContactCard => {
ReportSet::addressbook()
}
_ => ReportSet::file(),
};
fields.push(DavPropertyValue::new(property.clone(), reports));
}
WebDavProperty::CurrentUserPrincipal => {
@ -194,8 +192,8 @@ impl PrincipalPropFind for Server {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}",
DavResource::Principal.base_path(),
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
))],
));
@ -203,58 +201,67 @@ impl PrincipalPropFind for Server {
WebDavProperty::Group if !is_principal => {
fields.push(DavPropertyValue::empty(property.clone()));
}
WebDavProperty::CurrentUserPrivilegeSet if !is_principal => {
fields.push(DavPropertyValue::new(
property.clone(),
if access_token.is_member(account_id) {
Privilege::all(matches!(
collection,
Collection::Calendar | Collection::CalendarEvent
))
} else {
vec![Privilege::Read]
},
));
}
WebDavProperty::PrincipalCollectionSet => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(DavResource::Principal.base_path().to_string())],
vec![Href(
DavResourceName::Principal.collection_path().to_string(),
)],
));
}
_ => {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
},
DavProperty::Principal(principal_property) if is_principal => {
match principal_property {
PrincipalProperty::AlternateURISet => {
fields.push(DavPropertyValue::empty(property.clone()));
}
PrincipalProperty::GroupMemberSet => {
fields.push(DavPropertyValue::empty(property.clone()));
}
PrincipalProperty::GroupMembership => {
fields.push(DavPropertyValue::empty(property.clone()));
}
PrincipalProperty::PrincipalURL => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}",
DavResource::Principal.base_path(),
percent_encoding::utf8_percent_encode(
&name,
NON_ALPHANUMERIC
),
))],
));
}
PrincipalProperty::AddressbookHomeSet => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}",
DavResource::Card.base_path(),
percent_encoding::utf8_percent_encode(
&name,
NON_ALPHANUMERIC
),
))],
));
}
PrincipalProperty::PrincipalAddress => {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
DavProperty::Principal(principal_property) => match principal_property {
PrincipalProperty::AlternateURISet => {
fields.push(DavPropertyValue::empty(property.clone()));
}
}
PrincipalProperty::GroupMemberSet => {
fields.push(DavPropertyValue::empty(property.clone()));
}
PrincipalProperty::GroupMembership => {
fields.push(DavPropertyValue::empty(property.clone()));
}
PrincipalProperty::PrincipalURL => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
))],
));
}
PrincipalProperty::AddressbookHomeSet => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}/",
DavResourceName::Card.base_path(),
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
))],
));
response.set_namespace(Namespace::CardDav);
}
PrincipalProperty::PrincipalAddress => {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
response.set_namespace(Namespace::CardDav);
}
},
_ => {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
@ -274,7 +281,7 @@ impl PrincipalPropFind for Server {
response.add_response(Response::new_propstat(
Href(format!(
"{}/{}",
"{}/{}/",
base_path,
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
)),
@ -296,8 +303,8 @@ impl PrincipalPropFind for Server {
.caused_by(trc::location!())?
.unwrap_or_else(|| format!("_{account_id}"));
Ok(Href(format!(
"{}/{}",
DavResource::Principal.base_path(),
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
)))
}
@ -317,7 +324,7 @@ fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec<D
DavProperty::Principal(PrincipalProperty::GroupMemberSet),
DavProperty::Principal(PrincipalProperty::GroupMembership),
]
} else if let Some(all_props) = all_props {
} else {
let mut props = vec![
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::ResourceType),
@ -328,17 +335,11 @@ fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec<D
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
];
props.extend(all_props.iter().filter(|p| !p.is_all_prop()).cloned());
props
} else {
vec![
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
]
if let Some(all_props) = all_props {
props.extend(all_props.iter().filter(|p| !p.is_all_prop()).cloned());
props
} else {
props
}
}
}

View file

@ -13,10 +13,7 @@ use dav_proto::schema::{
request::{PrincipalPropertySearch, PropFind},
response::MultiStatus,
};
use directory::{
Type,
backend::internal::{PrincipalField, manage::ManageDirectory},
};
use directory::{Type, backend::internal::manage::ManageDirectory};
use http_proto::HttpResponse;
use hyper::StatusCode;
use jmap_proto::types::collection::Collection;

View file

@ -18,6 +18,7 @@ use dav_proto::{
BaseCondition, ErrorResponse, PrincipalSearchProperty, PrincipalSearchPropertySet,
},
},
xml_pretty_print,
};
use directory::Permission;
use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body};
@ -25,7 +26,7 @@ use hyper::{StatusCode, header};
use jmap_proto::types::collection::Collection;
use crate::{
DavError, DavMethod, DavResource,
DavError, DavMethod, DavResourceName,
card::{
copy_move::CardCopyMoveRequestHandler, delete::CardDeleteRequestHandler,
get::CardGetRequestHandler, mkcol::CardMkColRequestHandler,
@ -53,7 +54,7 @@ pub trait DavRequestHandler: Sync + Send {
request: HttpRequest,
access_token: Arc<AccessToken>,
session: &HttpSessionData,
resource: DavResource,
resource: DavResourceName,
method: DavMethod,
) -> impl Future<Output = HttpResponse> + Send;
}
@ -63,7 +64,7 @@ pub(crate) trait DavRequestDispatcher: Sync + Send {
&self,
request: &HttpRequest,
access_token: Arc<AccessToken>,
resource: DavResource,
resource: DavResourceName,
method: DavMethod,
body: Vec<u8>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
@ -74,7 +75,7 @@ impl DavRequestDispatcher for Server {
&self,
request: &HttpRequest,
access_token: Arc<AccessToken>,
resource: DavResource,
resource: DavResourceName,
method: DavMethod,
body: Vec<u8>,
) -> crate::Result<HttpResponse> {
@ -97,16 +98,18 @@ impl DavRequestDispatcher for Server {
DavMethod::PROPPATCH => {
let request = PropertyUpdate::parse(&mut Tokenizer::new(&body))?;
match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_proppatch_request(&access_token, headers, request)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_proppatch_request(&access_token, headers, request)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
DavMethod::MKCOL => {
@ -117,37 +120,39 @@ impl DavRequestDispatcher for Server {
};
match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_mkcol_request(&access_token, headers, request)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_mkcol_request(&access_token, headers, request)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
DavMethod::GET => match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_get_request(&access_token, headers, false)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_get_request(&access_token, headers, false)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::HEAD => match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_get_request(&access_token, headers, true)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
#[cfg(debug_assertions)]
{
// Deal with Litmus bug
@ -165,7 +170,7 @@ impl DavRequestDispatcher for Server {
.await
}
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::DELETE => {
// Include any fragments in the URI
@ -175,68 +180,70 @@ impl DavRequestDispatcher for Server {
}
match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_delete_request(&access_token, headers)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_delete_request(&access_token, headers)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
DavMethod::PUT | DavMethod::POST => match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_update_request(&access_token, headers, body, false)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_update_request(&access_token, headers, body, false)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::PATCH => match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_update_request(&access_token, headers, body, true)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_update_request(&access_token, headers, body, true)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::COPY => match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_copy_move_request(&access_token, headers, false)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_copy_move_request(&access_token, headers, false)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::MOVE => match resource {
DavResource::Card => {
DavResourceName::Card => {
self.handle_card_copy_move_request(&access_token, headers, false)
.await
}
DavResource::Cal => todo!(),
DavResource::File => {
DavResourceName::Cal => todo!(),
DavResourceName::File => {
self.handle_file_copy_move_request(&access_token, headers, true)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::LOCK => match resource {
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
_ => {
self.handle_lock_request(
&access_token,
@ -257,11 +264,13 @@ impl DavRequestDispatcher for Server {
DavMethod::ACL => {
let request = Acl::parse(&mut Tokenizer::new(&body))?;
match resource {
DavResource::Card | DavResource::Cal | DavResource::File => {
DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => {
self.handle_acl_request(&access_token, headers, request)
.await
}
DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
DavResourceName::Principal => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? {
@ -271,14 +280,14 @@ impl DavRequestDispatcher for Server {
.await
.and_then(|d| d.into_owned_uri())?;
match resource {
DavResource::Card | DavResource::Cal | DavResource::File => {
DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => {
self.handle_dav_query(
&access_token,
DavQuery::changes(uri, sync_collection, headers),
)
.await
}
DavResource::Principal => {
DavResourceName::Principal => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
@ -292,7 +301,7 @@ impl DavRequestDispatcher for Server {
.await
}
Report::PrincipalPropertySearch(report) => {
if resource == DavResource::Principal {
if resource == DavResourceName::Principal {
self.handle_principal_property_search(&access_token, report)
.await
} else {
@ -300,7 +309,7 @@ impl DavRequestDispatcher for Server {
}
}
Report::PrincipalSearchPropertySet => {
if resource == DavResource::Principal {
if resource == DavResourceName::Principal {
Ok(HttpResponse::new(StatusCode::OK).with_xml_body(
PrincipalSearchPropertySet::new(vec![PrincipalSearchProperty::new(
WebDavProperty::DisplayName,
@ -339,7 +348,7 @@ impl DavRequestHandler for Server {
mut request: HttpRequest,
access_token: Arc<AccessToken>,
session: &HttpSessionData,
resource: DavResource,
resource: DavResourceName,
method: DavMethod,
) -> HttpResponse {
let body = if method.has_body()
@ -375,6 +384,8 @@ impl DavRequestHandler for Server {
Vec::new()
};
let c = println!("------------------------------------------");
let std_body = std::str::from_utf8(&body).unwrap_or("[binary]").to_string();
let result = match self
@ -393,7 +404,13 @@ impl DavRequestHandler for Server {
) => HttpResponse::new(StatusCode::PRECONDITION_FAILED)
.with_xml_body(
ErrorResponse::new(BaseCondition::QuotaNotExceeded)
.with_namespace(resource)
.with_namespace(match resource {
DavResourceName::Card => Namespace::CardDav,
DavResourceName::Cal => Namespace::CalDav,
DavResourceName::File | DavResourceName::Principal => {
Namespace::Dav
}
})
.to_string(),
)
.with_no_cache(),
@ -417,7 +434,11 @@ impl DavRequestHandler for Server {
Err(DavError::Condition(condition)) => HttpResponse::new(condition.code)
.with_xml_body(
ErrorResponse::new(condition.condition)
.with_namespace(resource)
.with_namespace(match resource {
DavResourceName::Card => Namespace::CardDav,
DavResourceName::Cal => Namespace::CalDav,
DavResourceName::File | DavResourceName::Principal => Namespace::Dav,
})
.to_string(),
)
.with_no_cache(),
@ -425,7 +446,7 @@ impl DavRequestHandler for Server {
};
let c = println!(
"------------------------------------------\n{:?} {} -> {:?}\nHeaders: {:?}\nBody: {}\nResponse headers: {:?}\nResponse: {}",
"{:?} {} -> {:?}\nHeaders: {:?}\nBody: {}\nResponse headers: {:?}\nResponse: {}",
method,
request.uri().path(),
result.status(),
@ -433,9 +454,9 @@ impl DavRequestHandler for Server {
std_body,
result.headers().unwrap(),
match &result.body() {
http_proto::HttpResponseBody::Text(t) => t,
http_proto::HttpResponseBody::Empty => "[empty]",
_ => "[binary]",
http_proto::HttpResponseBody::Text(t) => xml_pretty_print(t),
http_proto::HttpResponseBody::Empty => "[empty]".to_string(),
_ => "[binary]".to_string(),
}
);
@ -454,13 +475,3 @@ impl From<trc::Error> for DavError {
DavError::Internal(err)
}
}
impl From<DavResource> for Namespace {
fn from(value: DavResource) -> Self {
match value {
DavResource::Card => Namespace::CardDav,
DavResource::Cal => Namespace::CalDav,
DavResource::File | DavResource::Principal => Namespace::Dav,
}
}
}

View file

@ -310,7 +310,7 @@ impl Principal {
typ: Type::Individual,
name: "Fallback Administrator".into(),
secrets: vec![fallback_pass.into()],
data: vec![PrincipalData::MemberOf(vec![ROLE_ADMIN])],
data: vec![PrincipalData::Roles(vec![ROLE_ADMIN])],
description: Default::default(),
emails: Default::default(),
quota: Default::default(),

View file

@ -7,7 +7,7 @@
use common::storage::index::{
IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject,
};
use jmap_proto::types::value::AclGrant;
use jmap_proto::types::{collection::Collection, value::AclGrant};
use store::SerializeInfallible;
use crate::{IDX_CARD_UID, IDX_NAME};
@ -89,6 +89,10 @@ impl IndexableObject for ContactCard {
+ self.size,
},
IndexValue::LogChild { prefix: None },
IndexValue::LogParent {
collection: Collection::AddressBook.into(),
ids: self.names.iter().map(|n| n.parent_id).collect(),
},
]
.into_iter()
}
@ -116,6 +120,10 @@ impl IndexableObject for &ArchivedContactCard {
+ self.size,
},
IndexValue::LogChild { prefix: None },
IndexValue::LogParent {
collection: Collection::AddressBook.into(),
ids: self.names.iter().map(|n| n.parent_id.to_native()).collect(),
},
]
.into_iter()
}

View file

@ -5,6 +5,7 @@
*/
pub mod index;
pub mod storage;
use calcard::vcard::VCard;

View file

@ -0,0 +1,231 @@
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use jmap_proto::types::collection::Collection;
use store::write::{Archive, BatchBuilder, now};
use trc::AddContext;
use crate::DestroyArchive;
use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard};
impl ContactCard {
pub fn update<'x>(
self,
access_token: &AccessToken,
card: Archive<&ArchivedContactCard>,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
let mut new_card = self;
// Build card
new_card.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(card)
.with_changes(new_card)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
pub fn insert<'x>(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build card
let mut card = self;
let now = now() as i64;
card.modified = now;
card.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(card)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
}
impl AddressBook {
pub fn insert<'x>(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build address book
let mut book = self;
let now = now() as i64;
book.modified = now;
book.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(book)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
pub fn update<'x>(
self,
access_token: &AccessToken,
book: Archive<&ArchivedAddressBook>,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build address book
let mut new_book = self;
new_book.modified = now() as i64;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(book)
.with_changes(new_book)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
}
impl DestroyArchive<Archive<&ArchivedAddressBook>> {
pub async fn delete_with_cards(
self,
server: &Server,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
children_ids: Vec<u32>,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Process deletions
let addressbook_id = document_id;
for document_id in children_ids {
if let Some(card_) = server
.get_archive(account_id, Collection::ContactCard, document_id)
.await?
{
DestroyArchive(
card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?,
)
.delete(
access_token,
account_id,
document_id,
addressbook_id,
batch,
)?;
}
}
self.delete(access_token, account_id, document_id, batch)
}
pub fn delete(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let book = self.0;
// Delete addressbook
batch
.with_account_id(account_id)
.with_collection(Collection::AddressBook)
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(book),
)
.caused_by(trc::location!())?
.commit_point();
Ok(())
}
}
impl DestroyArchive<Archive<&ArchivedContactCard>> {
pub fn delete(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
addressbook_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
let card = self.0;
if let Some(delete_idx) = card
.inner
.names
.iter()
.position(|name| name.parent_id == addressbook_id)
{
batch
.with_account_id(account_id)
.with_collection(Collection::ContactCard);
if card.inner.names.len() > 1 {
// Unlink addressbook id from card
let mut new_card = card
.deserialize::<ContactCard>()
.caused_by(trc::location!())?;
new_card.names.swap_remove(delete_idx);
batch
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_tenant_id(access_token)
.with_current(card)
.with_changes(new_card),
)
.caused_by(trc::location!())?;
} else {
// Delete card
batch
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(card),
)
.caused_by(trc::location!())?;
}
batch.commit_point();
}
Ok(())
}
}

View file

@ -5,7 +5,7 @@
*/
pub mod index;
pub mod storage;
use dav_proto::schema::request::DeadProperty;
use jmap_proto::types::value::AclGrant;

View file

@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use jmap_proto::types::collection::Collection;
use store::write::{Archive, BatchBuilder, now};
use trc::AddContext;
use crate::DestroyArchive;
use super::{ArchivedFileNode, FileNode};
impl FileNode {
pub fn insert<'x>(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build node
let mut node = self;
let now = now() as i64;
node.modified = now;
node.created = now;
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.create_document(document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(node)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
pub fn update<'x>(
self,
access_token: &AccessToken,
node: Archive<&ArchivedFileNode>,
account_id: u32,
document_id: u32,
batch: &'x mut BatchBuilder,
) -> trc::Result<&'x mut BatchBuilder> {
// Build node
let mut new_node = self;
new_node.modified = now() as i64;
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.update_document(document_id)
.custom(
ObjectIndexBuilder::new()
.with_current(node)
.with_changes(new_node)
.with_tenant_id(access_token),
)
.map(|b| b.commit_point())
}
}
impl DestroyArchive<Archive<&ArchivedFileNode>> {
pub fn delete(
self,
access_token: &AccessToken,
account_id: u32,
document_id: u32,
batch: &mut BatchBuilder,
) -> trc::Result<()> {
// Prepare write batch
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_current(self.0)
.with_tenant_id(access_token),
)?
.commit_point();
Ok(())
}
}
impl DestroyArchive<Vec<u32>> {
pub async fn delete(
self,
server: &Server,
access_token: &AccessToken,
account_id: u32,
) -> trc::Result<()> {
// Process deletions
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode);
for document_id in self.0 {
if let Some(node) = server
.get_archive(account_id, Collection::FileNode, document_id)
.await?
{
// Delete record
batch
.delete_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_tenant_id(access_token)
.with_current(
node.to_unarchived::<FileNode>()
.caused_by(trc::location!())?,
),
)
.caused_by(trc::location!())?
.commit_point();
}
}
// Write changes
if !batch.is_empty() {
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
}
Ok(())
}
}

View file

@ -6,28 +6,45 @@
use std::sync::Arc;
use common::{DavResource, DavResourceId, DavResources, Server};
use common::{DavResource, DavResourceId, DavResources, Server, auth::AccessToken};
use directory::backend::internal::manage::ManageDirectory;
use jmap_proto::types::collection::Collection;
use percent_encoding::NON_ALPHANUMERIC;
use store::{
Deserialize, IndexKey, IterateParams, SerializeInfallible, U32_LEN, ahash::AHashMap,
write::key::DeserializeBigEndian,
Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN,
ahash::AHashMap,
write::{BatchBuilder, key::DeserializeBigEndian},
};
use trc::AddContext;
use utils::bimap::IdBimap;
use crate::{DavName, IDX_NAME, file::FileNode};
use crate::{DavName, DavResourceName, IDX_NAME, contact::AddressBook, file::FileNode};
pub trait DavHierarchy: Sync + Send {
fn fetch_dav_resources(
&self,
access_token: &AccessToken,
account_id: u32,
collection: Collection,
) -> impl Future<Output = trc::Result<Arc<DavResources>>> + Send;
fn create_default_addressbook(
&self,
access_token: &AccessToken,
account_id: u32,
) -> impl Future<Output = trc::Result<()>> + Send;
fn create_default_calendar(
&self,
access_token: &AccessToken,
account_id: u32,
) -> impl Future<Output = trc::Result<()>> + Send;
}
impl DavHierarchy for Server {
async fn fetch_dav_resources(
&self,
access_token: &AccessToken,
account_id: u32,
collection: Collection,
) -> trc::Result<Arc<DavResources>> {
@ -51,7 +68,23 @@ impl DavHierarchy for Server {
} else {
let mut files = match collection {
Collection::Calendar | Collection::AddressBook => {
build_hierarchy(self, account_id, collection).await?
let files = build_hierarchy(self, account_id, collection).await?;
if files.paths.is_empty() {
match collection {
Collection::Calendar => {
self.create_default_calendar(access_token, account_id)
.await?
}
Collection::AddressBook => {
self.create_default_addressbook(access_token, account_id)
.await?
}
_ => unreachable!(),
}
build_hierarchy(self, account_id, collection).await?
} else {
files
}
}
Collection::FileNode => build_file_hierarchy(self, account_id).await?,
_ => unreachable!(),
@ -64,6 +97,38 @@ impl DavHierarchy for Server {
Ok(files)
}
}
async fn create_default_addressbook(
&self,
access_token: &AccessToken,
account_id: u32,
) -> trc::Result<()> {
if let Some(name) = &self.core.dav.default_addressbook_name {
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::AddressBook, 1)
.await?;
AddressBook {
name: name.clone(),
display_name: self.core.dav.default_addressbook_display_name.clone(),
is_default: true,
..Default::default()
}
.insert(access_token, account_id, document_id, &mut batch)?;
self.commit_batch(batch).await?;
}
Ok(())
}
async fn create_default_calendar(
&self,
access_token: &AccessToken,
account_id: u32,
) -> trc::Result<()> {
todo!()
}
}
async fn build_hierarchy(
@ -71,6 +136,7 @@ async fn build_hierarchy(
account_id: u32,
collection: Collection,
) -> trc::Result<DavResources> {
let base_path = DavResourceName::from(collection).base_path();
let collection = u8::from(collection);
let mut containers: AHashMap<u32, String> = AHashMap::with_capacity(16);
let mut resources: AHashMap<u32, Vec<DavName>> = AHashMap::with_capacity(16);
@ -99,7 +165,7 @@ async fn build_hierarchy(
|key, _| {
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
let value = key
.get(key.len() - (U32_LEN * 2)..key.len() - U32_LEN)
.get(IndexKeyPrefix::len()..key.len() - U32_LEN)
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
let key_collection = key
.get(U32_LEN)
@ -126,10 +192,22 @@ async fn build_hierarchy(
.await
.caused_by(trc::location!())?;
let name = server
.store()
.get_principal_name(account_id)
.await
.caused_by(trc::location!())?
.unwrap_or_else(|| format!("_{account_id}"));
let mut files = DavResources {
paths: IdBimap::with_capacity(containers.len() + resources.len()),
size: std::mem::size_of::<DavResources>() as u64,
modseq: None,
base_path: format!(
"{}/{}/",
base_path,
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
),
};
for (document_id, dav_names) in resources {
@ -172,7 +250,18 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<D
.fetch_folders::<FileNode>(account_id, Collection::FileNode)
.await
.caused_by(trc::location!())?;
let name = server
.store()
.get_principal_name(account_id)
.await
.caused_by(trc::location!())?
.unwrap_or_else(|| format!("_{account_id}"));
let mut files = DavResources {
base_path: format!(
"{}/{}/",
DavResourceName::Card.base_path(),
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
),
paths: IdBimap::with_capacity(list.len()),
size: std::mem::size_of::<DavResources>() as u64,
modseq: None,

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use jmap_proto::types::collection::Collection;
use store::{Deserialize, SerializeInfallible, write::key::KeySerializer};
use utils::codec::leb128::Leb128Reader;
@ -15,6 +16,16 @@ pub mod hierarchy;
pub const IDX_NAME: u8 = 0;
pub const IDX_CARD_UID: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DavResourceName {
Card,
Cal,
File,
Principal,
}
pub struct DestroyArchive<T>(pub T);
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
@ -69,3 +80,55 @@ impl Deserialize for DavName {
Ok(DavName { name, parent_id })
}
}
impl DavResourceName {
pub fn parse(service: &str) -> Option<Self> {
hashify::tiny_map!(service.as_bytes(),
"card" => DavResourceName::Card,
"cal" => DavResourceName::Cal,
"file" => DavResourceName::File,
"pal" => DavResourceName::Principal,
)
}
pub fn base_path(&self) -> &'static str {
match self {
DavResourceName::Card => "/dav/card",
DavResourceName::Cal => "/dav/cal",
DavResourceName::File => "/dav/file",
DavResourceName::Principal => "/dav/pal",
}
}
pub fn collection_path(&self) -> &'static str {
match self {
DavResourceName::Card => "/dav/card/",
DavResourceName::Cal => "/dav/cal/",
DavResourceName::File => "/dav/file/",
DavResourceName::Principal => "/dav/pal/",
}
}
}
impl From<DavResourceName> for Collection {
fn from(value: DavResourceName) -> Self {
match value {
DavResourceName::Card => Collection::AddressBook,
DavResourceName::Cal => Collection::Calendar,
DavResourceName::File => Collection::FileNode,
DavResourceName::Principal => Collection::Principal,
}
}
}
impl From<Collection> for DavResourceName {
fn from(value: Collection) -> Self {
match value {
Collection::AddressBook => DavResourceName::Card,
Collection::Calendar => DavResourceName::Cal,
Collection::FileNode => DavResourceName::File,
Collection::Principal => DavResourceName::Principal,
_ => unreachable!(),
}
}
}

View file

@ -13,6 +13,7 @@ email = { path = "../email" }
smtp = { path = "../smtp" }
jmap = { path = "../jmap" }
dav = { path = "../dav" }
groupware = { path = "../groupware" }
spam-filter = { path = "../spam-filter" }
http_proto = { path = "../http-proto" }
jmap_proto = { path = "../jmap-proto" }

View file

@ -14,8 +14,9 @@ use common::{
listener::{SessionData, SessionManager, SessionStream},
manager::webadmin::Resource,
};
use dav::{DavMethod, DavResource, request::DavRequestHandler};
use dav::{DavMethod, request::DavRequestHandler};
use directory::Permission;
use groupware::DavResourceName;
use http_proto::{
DownloadResponse, HttpContext, HttpRequest, HttpResponse, HttpResponseBody, HttpSessionData,
JsonProblemResponse, ToHttpResponse, form_urlencoded, request::fetch_body,
@ -206,12 +207,21 @@ impl ParseHttp for Server {
}
"dav" => {
let response = match (
path.next().and_then(DavResource::parse),
path.next().and_then(DavResourceName::parse),
DavMethod::parse(req.method()),
) {
(Some(resource), Some(DavMethod::OPTIONS)) => {
resource.into_options_response(path.count())
}
(Some(_), Some(DavMethod::OPTIONS)) => HttpResponse::new(StatusCode::OK)
.with_header(
"DAV",
"1, 2, 3, access-control, extended-mkcol, calendar-access, addressbook",
)
.with_header(
"Allow",
concat!(
"OPTIONS, GET, HEAD, POST, PUT, DELETE, COPY, MOVE, MKCALENDAR, ",
"MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL"
),
),
(Some(resource), Some(method)) => {
// Authenticate request
let (_in_flight, access_token) =
@ -237,17 +247,15 @@ impl ParseHttp for Server {
.await
.map(|s| s.into_http_response());
}
("caldav", &Method::GET) => {
let base_url = ctx.resolve_response_url(self).await;
("caldav", _) => {
return Ok(HttpResponse::new(StatusCode::TEMPORARY_REDIRECT)
.with_no_cache()
.with_location(format!("{base_url}/dav/cal")));
.with_location(DavResourceName::Cal.base_path()));
}
("carddav", &Method::GET) => {
let base_url = ctx.resolve_response_url(self).await;
("carddav", _) => {
return Ok(HttpResponse::new(StatusCode::TEMPORARY_REDIRECT)
.with_no_cache()
.with_location(format!("{base_url}/dav/card")));
.with_location(DavResourceName::Card.base_path()));
}
("oauth-authorization-server", &Method::GET) => {
// Limit anonymous requests

View file

@ -47,6 +47,10 @@ impl<T: IdBimapItem> IdBimap<T> {
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.name_to_id.values().map(|v| v.as_ref())
}
pub fn is_empty(&self) -> bool {
self.name_to_id.is_empty()
}
}
// SAFETY: Safe because Rc<> are never returned from the struct