mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-11-17 22:29:31 +08:00
CardDAV working with Thunderbird and Apple Contacts
This commit is contained in:
parent
ce27cecded
commit
10ae19f2eb
53 changed files with 1544 additions and 1233 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3073,6 +3073,7 @@ dependencies = [
|
|||
"directory",
|
||||
"email",
|
||||
"form-data",
|
||||
"groupware",
|
||||
"http-body-util",
|
||||
"http_proto",
|
||||
"hyper 1.6.0",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,13 @@ impl NamedElement {
|
|||
element,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calendarserver(element: Element) -> NamedElement {
|
||||
NamedElement {
|
||||
ns: Namespace::CalendarServer,
|
||||
element,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Token<'_> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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_)?;
|
||||
|
|
|
|||
|
|
@ -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_)?;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
pub mod index;
|
||||
pub mod storage;
|
||||
|
||||
use calcard::vcard::VCard;
|
||||
|
||||
|
|
|
|||
231
crates/groupware/src/contact/storage.rs
Normal file
231
crates/groupware/src/contact/storage.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
pub mod index;
|
||||
|
||||
pub mod storage;
|
||||
|
||||
use dav_proto::schema::request::DeadProperty;
|
||||
use jmap_proto::types::value::AclGrant;
|
||||
|
|
|
|||
133
crates/groupware/src/file/storage.rs
Normal file
133
crates/groupware/src/file/storage.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue