diff --git a/Cargo.lock b/Cargo.lock index 3a835f76..b3e6ffbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1014,6 +1014,7 @@ dependencies = [ "hashify", "mail-builder", "mail-parser", + "serde", ] [[package]] @@ -1696,6 +1697,8 @@ dependencies = [ "mail-parser", "quick-xml 0.37.2", "rkyv 0.8.10", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b6477019..1e4cad1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "crates/smtp", "crates/managesieve", "crates/pop3", + "crates/dav-proto", "crates/dav", "crates/groupware", "crates/spam-filter", diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml new file mode 100644 index 00000000..182105b2 --- /dev/null +++ b/crates/dav-proto/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dav-proto" +version = "0.1.0" +edition = "2021" + +[dependencies] +hashify = "0.2.6" +quick-xml = "0.37.2" +calcard = { path = "/Users/me/code/calcard" } +mail-parser = "0.10.2" +hyper = "1.6.0" +rkyv = { version = "0.8.10", features = ["little_endian"] } + +[dev-dependencies] +calcard = { path = "/Users/me/code/calcard", features = ["serde"] } +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.138" diff --git a/crates/dav-proto/resources/requests/acl-001.json b/crates/dav-proto/resources/requests/acl-001.json new file mode 100644 index 00000000..56d5f5e0 --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-001.json @@ -0,0 +1,30 @@ +{ + "aces": [ + { + "principal": { + "Href": "http://www.example.com/users/friends" + }, + "invert": false, + "grant_deny": { + "Grant": [ + "Read" + ] + }, + "protected": false, + "inherited": null + }, + { + "principal": { + "Href": "http://www.example.com/users/ygoland-so" + }, + "invert": false, + "grant_deny": { + "Deny": [ + "Read" + ] + }, + "protected": false, + "inherited": null + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/acl-001.xml b/crates/dav-proto/resources/requests/acl-001.xml new file mode 100644 index 00000000..1853ad1c --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-001.xml @@ -0,0 +1,15 @@ + + + + + http://www.example.com/users/friends + + + + + + http://www.example.com/users/ygoland-so + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/acl-002.json b/crates/dav-proto/resources/requests/acl-002.json new file mode 100644 index 00000000..ccf9b500 --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-002.json @@ -0,0 +1,17 @@ +{ + "aces": [ + { + "principal": { + "Href": "http://www.example.com/users/ejw" + }, + "invert": false, + "grant_deny": { + "Grant": [ + "Write" + ] + }, + "protected": false, + "inherited": null + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/acl-002.xml b/crates/dav-proto/resources/requests/acl-002.xml new file mode 100644 index 00000000..f97bc128 --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-002.xml @@ -0,0 +1,9 @@ + + + + + http://www.example.com/users/ejw + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/acl-003.json b/crates/dav-proto/resources/requests/acl-003.json new file mode 100644 index 00000000..9b8648c4 --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-003.json @@ -0,0 +1,17 @@ +{ + "aces": [ + { + "principal": { + "Href": "http://www.example.com/users/esedlar" + }, + "invert": true, + "grant_deny": { + "Deny": [ + "Write" + ] + }, + "protected": true, + "inherited": "http://www.example.com/container/" + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/acl-003.xml b/crates/dav-proto/resources/requests/acl-003.xml new file mode 100644 index 00000000..d62274bb --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-003.xml @@ -0,0 +1,15 @@ + + + + + http://www.example.com/users/esedlar + + + + + + + http://www.example.com/container/ + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/acl-004.json b/crates/dav-proto/resources/requests/acl-004.json new file mode 100644 index 00000000..b56597e4 --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-004.json @@ -0,0 +1,53 @@ +{ + "aces": [ + { + "principal": { + "Href": "http://www.example.com/users/esedlar" + }, + "invert": false, + "grant_deny": { + "Grant": [ + "Read", + "Write" + ] + }, + "protected": false, + "inherited": null + }, + { + "principal": { + "Property": [ + { + "property": { + "type": "WebDav", + "data": { + "type": "Owner" + } + }, + "value": "Null" + } + ] + }, + "invert": false, + "grant_deny": { + "Grant": [ + "ReadAcl", + "WriteAcl" + ] + }, + "protected": false, + "inherited": null + }, + { + "principal": "All", + "invert": false, + "grant_deny": { + "Grant": [ + "Read" + ] + }, + "protected": false, + "inherited": null + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/acl-004.xml b/crates/dav-proto/resources/requests/acl-004.xml new file mode 100644 index 00000000..9414370f --- /dev/null +++ b/crates/dav-proto/resources/requests/acl-004.xml @@ -0,0 +1,27 @@ + + + + + http://www.example.com/users/esedlar + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/lockinfo-001.json b/crates/dav-proto/resources/requests/lockinfo-001.json new file mode 100644 index 00000000..ae2b2587 --- /dev/null +++ b/crates/dav-proto/resources/requests/lockinfo-001.json @@ -0,0 +1,20 @@ +{ + "lock_scope": "Shared", + "lock_type": "Write", + "owner": [ + { + "type": "ElementStart", + "data": { + "name": "D:href", + "attrs": null + } + }, + { + "type": "Text", + "data": "http://example.org/~ejw/contact.html" + }, + { + "type": "ElementEnd" + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/lockinfo-001.xml b/crates/dav-proto/resources/requests/lockinfo-001.xml new file mode 100644 index 00000000..5a9b42e3 --- /dev/null +++ b/crates/dav-proto/resources/requests/lockinfo-001.xml @@ -0,0 +1,8 @@ + + + + + + http://example.org/~ejw/contact.html + + diff --git a/crates/dav-proto/resources/requests/lockinfo-002.json b/crates/dav-proto/resources/requests/lockinfo-002.json new file mode 100644 index 00000000..e405d67d --- /dev/null +++ b/crates/dav-proto/resources/requests/lockinfo-002.json @@ -0,0 +1,20 @@ +{ + "lock_scope": "Exclusive", + "lock_type": "Write", + "owner": [ + { + "type": "ElementStart", + "data": { + "name": "D:href", + "attrs": null + } + }, + { + "type": "Text", + "data": "http://example.org/~ejw/contact.html" + }, + { + "type": "ElementEnd" + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/lockinfo-002.xml b/crates/dav-proto/resources/requests/lockinfo-002.xml new file mode 100644 index 00000000..05fc33af --- /dev/null +++ b/crates/dav-proto/resources/requests/lockinfo-002.xml @@ -0,0 +1,9 @@ + + + + + + http://example.org/~ejw/contact.html + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/mkcol-001.json b/crates/dav-proto/resources/requests/mkcol-001.json new file mode 100644 index 00000000..9265cdfd --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-001.json @@ -0,0 +1,41 @@ +{ + "is_mkcalendar": false, + "props": [ + { + "property": { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + }, + "value": { + "ResourceTypes": [ + "Collection", + "AddressBook" + ] + } + }, + { + "property": { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + "value": { + "String": "Lisa's Contacts" + } + }, + { + "property": { + "type": "CardDav", + "data": { + "type": "AddressbookDescription" + } + }, + "value": { + "String": "My primary address book." + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/mkcol-001.xml b/crates/dav-proto/resources/requests/mkcol-001.xml new file mode 100644 index 00000000..c7dff1bb --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-001.xml @@ -0,0 +1,15 @@ + + + + + + + + + Lisa's Contacts + My primary address book. + + + diff --git a/crates/dav-proto/resources/requests/mkcol-002.json b/crates/dav-proto/resources/requests/mkcol-002.json new file mode 100644 index 00000000..130de28e --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-002.json @@ -0,0 +1,358 @@ +{ + "is_mkcalendar": true, + "props": [ + { + "property": { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + "value": { + "String": "Lisa's Events" + } + }, + { + "property": { + "type": "CalDav", + "data": { + "type": "CalendarDescription" + } + }, + "value": { + "String": "Calendar restricted to events." + } + }, + { + "property": { + "type": "CalDav", + "data": { + "type": "SupportedCalendarComponentSet" + } + }, + "value": { + "Components": [ + "VEvent" + ] + } + }, + { + "property": { + "type": "CalDav", + "data": { + "type": "CalendarTimezone" + } + }, + "value": { + "ICalendar": { + "component_type": "VCalendar", + "entries": [ + { + "name": { + "type": "Prodid" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "-//Example Corp.//CalDAV Client//EN" + } + ] + }, + { + "name": { + "type": "Version" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "2.0" + } + ] + } + ], + "components": [ + { + "component_type": "VTimezone", + "entries": [ + { + "name": { + "type": "Tzid" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "US-Eastern" + } + ] + }, + { + "name": { + "type": "LastModified" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": 1987, + "month": 1, + "day": 1, + "hour": 0, + "minute": 0, + "second": 0, + "tz_hour": 0, + "tz_minute": 0, + "tz_minus": false + } + } + ] + } + ], + "components": [ + { + "component_type": "Standard", + "entries": [ + { + "name": { + "type": "Dtstart" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": 1967, + "month": 10, + "day": 29, + "hour": 2, + "minute": 0, + "second": 0, + "tz_hour": null, + "tz_minute": null, + "tz_minus": false + } + } + ] + }, + { + "name": { + "type": "Rrule" + }, + "params": [], + "values": [ + { + "type": "RecurrenceRule", + "data": { + "freq": "Yearly", + "until": null, + "count": null, + "interval": null, + "bysecond": [], + "byminute": [], + "byhour": [], + "byday": [ + { + "ordwk": -1, + "weekday": "Sunday" + } + ], + "bymonthday": [], + "byyearday": [], + "byweekno": [], + "bymonth": [ + 10 + ], + "bysetpos": [], + "wkst": null + } + } + ] + }, + { + "name": { + "type": "Tzoffsetfrom" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 4, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzoffsetto" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 5, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzname" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "Eastern Standard Time (US & Canada)" + } + ] + } + ], + "components": [] + }, + { + "component_type": "Daylight", + "entries": [ + { + "name": { + "type": "Dtstart" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": 1987, + "month": 4, + "day": 5, + "hour": 2, + "minute": 0, + "second": 0, + "tz_hour": null, + "tz_minute": null, + "tz_minus": false + } + } + ] + }, + { + "name": { + "type": "Rrule" + }, + "params": [], + "values": [ + { + "type": "RecurrenceRule", + "data": { + "freq": "Yearly", + "until": null, + "count": null, + "interval": null, + "bysecond": [], + "byminute": [], + "byhour": [], + "byday": [ + { + "ordwk": 1, + "weekday": "Sunday" + } + ], + "bymonthday": [], + "byyearday": [], + "byweekno": [], + "bymonth": [ + 4 + ], + "bysetpos": [], + "wkst": null + } + } + ] + }, + { + "name": { + "type": "Tzoffsetfrom" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 5, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzoffsetto" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 4, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzname" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "Eastern Daylight Time (US & Canada)" + } + ] + } + ], + "components": [] + } + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/mkcol-002.xml b/crates/dav-proto/resources/requests/mkcol-002.xml new file mode 100644 index 00000000..d31a33db --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-002.xml @@ -0,0 +1,37 @@ + + + + + Lisa's Events + Calendar restricted to events. + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/mkcol-003.json b/crates/dav-proto/resources/requests/mkcol-003.json new file mode 100644 index 00000000..c56383b0 --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-003.json @@ -0,0 +1,29 @@ +{ + "is_mkcalendar": false, + "props": [ + { + "property": { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + }, + "value": { + "ResourceTypes": [ + "Collection" + ] + } + }, + { + "property": { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + "value": { + "String": "Special Resource" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/mkcol-003.xml b/crates/dav-proto/resources/requests/mkcol-003.xml new file mode 100644 index 00000000..0d7e13a7 --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-003.xml @@ -0,0 +1,14 @@ + + + + + + + + + Special Resource + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/mkcol-004.json b/crates/dav-proto/resources/requests/mkcol-004.json new file mode 100644 index 00000000..5692c8e7 --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-004.json @@ -0,0 +1,30 @@ +{ + "is_mkcalendar": false, + "props": [ + { + "property": { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + }, + "value": { + "ResourceTypes": [ + "Collection", + "Calendar" + ] + } + }, + { + "property": { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + "value": { + "String": "Lisa's Events" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/mkcol-004.xml b/crates/dav-proto/resources/requests/mkcol-004.xml new file mode 100644 index 00000000..a7d09fd1 --- /dev/null +++ b/crates/dav-proto/resources/requests/mkcol-004.xml @@ -0,0 +1,13 @@ + + + + + + + + + Lisa's Events + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propertyupdate-001.json b/crates/dav-proto/resources/requests/propertyupdate-001.json new file mode 100644 index 00000000..e89c3cf2 --- /dev/null +++ b/crates/dav-proto/resources/requests/propertyupdate-001.json @@ -0,0 +1,386 @@ +{ + "set": [ + { + "property": { + "type": "CardDav", + "data": { + "type": "AddressbookDescription" + } + }, + "value": { + "String": "Adresses de Oliver Daboo" + } + }, + { + "property": { + "type": "CalDav", + "data": { + "type": "CalendarDescription" + } + }, + "value": { + "String": "Calendrier de Mathilde Desruisseaux" + } + }, + { + "property": { + "type": "CalDav", + "data": { + "type": "SupportedCalendarComponentSet" + } + }, + "value": { + "Components": [ + "VEvent", + "VTodo" + ] + } + }, + { + "property": { + "type": "CalDav", + "data": { + "type": "CalendarTimezone" + } + }, + "value": { + "ICalendar": { + "component_type": "VCalendar", + "entries": [ + { + "name": { + "type": "Prodid" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "-//Example Corp.//CalDAV Client//EN" + } + ] + }, + { + "name": { + "type": "Version" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "2.0" + } + ] + } + ], + "components": [ + { + "component_type": "VTimezone", + "entries": [ + { + "name": { + "type": "Tzid" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "US-Eastern" + } + ] + }, + { + "name": { + "type": "LastModified" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": 1987, + "month": 1, + "day": 1, + "hour": 0, + "minute": 0, + "second": 0, + "tz_hour": 0, + "tz_minute": 0, + "tz_minus": false + } + } + ] + } + ], + "components": [ + { + "component_type": "Standard", + "entries": [ + { + "name": { + "type": "Dtstart" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": 1967, + "month": 10, + "day": 29, + "hour": 2, + "minute": 0, + "second": 0, + "tz_hour": null, + "tz_minute": null, + "tz_minus": false + } + } + ] + }, + { + "name": { + "type": "Rrule" + }, + "params": [], + "values": [ + { + "type": "RecurrenceRule", + "data": { + "freq": "Yearly", + "until": null, + "count": null, + "interval": null, + "bysecond": [], + "byminute": [], + "byhour": [], + "byday": [ + { + "ordwk": -1, + "weekday": "Sunday" + } + ], + "bymonthday": [], + "byyearday": [], + "byweekno": [], + "bymonth": [ + 10 + ], + "bysetpos": [], + "wkst": null + } + } + ] + }, + { + "name": { + "type": "Tzoffsetfrom" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 4, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzoffsetto" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 5, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzname" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "Eastern Standard Time (US & Canada)" + } + ] + } + ], + "components": [] + }, + { + "component_type": "Daylight", + "entries": [ + { + "name": { + "type": "Dtstart" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": 1987, + "month": 4, + "day": 5, + "hour": 2, + "minute": 0, + "second": 0, + "tz_hour": null, + "tz_minute": null, + "tz_minus": false + } + } + ] + }, + { + "name": { + "type": "Rrule" + }, + "params": [], + "values": [ + { + "type": "RecurrenceRule", + "data": { + "freq": "Yearly", + "until": null, + "count": null, + "interval": null, + "bysecond": [], + "byminute": [], + "byhour": [], + "byday": [ + { + "ordwk": 1, + "weekday": "Sunday" + } + ], + "bymonthday": [], + "byyearday": [], + "byweekno": [], + "bymonth": [ + 4 + ], + "bysetpos": [], + "wkst": null + } + } + ] + }, + { + "name": { + "type": "Tzoffsetfrom" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 5, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzoffsetto" + }, + "params": [], + "values": [ + { + "type": "PartialDateTime", + "data": { + "year": null, + "month": null, + "day": null, + "hour": null, + "minute": null, + "second": null, + "tz_hour": 4, + "tz_minute": 0, + "tz_minus": true + } + } + ] + }, + { + "name": { + "type": "Tzname" + }, + "params": [], + "values": [ + { + "type": "Text", + "data": "Eastern Daylight Time (US & Canada)" + } + ] + } + ], + "components": [] + } + ] + } + ] + } + } + }, + { + "property": { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + }, + "value": { + "ResourceTypes": [ + "Collection", + "AddressBook" + ] + } + } + ], + "remove": [ + { + "type": "CalDav", + "data": { + "type": "CalendarTimezone" + } + }, + { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propertyupdate-001.xml b/crates/dav-proto/resources/requests/propertyupdate-001.xml new file mode 100644 index 00000000..3b92ee96 --- /dev/null +++ b/crates/dav-proto/resources/requests/propertyupdate-001.xml @@ -0,0 +1,55 @@ + + + + + Adresses de Oliver Daboo + + Calendrier de Mathilde Desruisseaux + + + + + BEGIN:VCALENDAR +PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:US-Eastern +LAST-MODIFIED:19870101T000000Z +BEGIN:STANDARD +DTSTART:19671029T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:Eastern Standard Time (US & Canada) +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:19870405T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:Eastern Daylight Time (US & Canada) +END:DAYLIGHT +END:VTIMEZONE +END:VCALENDAR + + + + + + + + + + + + + + diff --git a/crates/dav-proto/resources/requests/propfind-001.json b/crates/dav-proto/resources/requests/propfind-001.json new file mode 100644 index 00000000..0cb6e986 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-001.json @@ -0,0 +1,33 @@ +{ + "type": "Prop", + "data": [ + { + "type": "DeadProperty", + "data": { + "name": "R:bigbox", + "attrs": null + } + }, + { + "type": "DeadProperty", + "data": { + "name": "R:author", + "attrs": null + } + }, + { + "type": "DeadProperty", + "data": { + "name": "R:DingALing", + "attrs": null + } + }, + { + "type": "DeadProperty", + "data": { + "name": "R:Random", + "attrs": null + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-001.xml b/crates/dav-proto/resources/requests/propfind-001.xml new file mode 100644 index 00000000..4f719419 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-001.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/crates/dav-proto/resources/requests/propfind-002.json b/crates/dav-proto/resources/requests/propfind-002.json new file mode 100644 index 00000000..7d15ff6b --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-002.json @@ -0,0 +1,3 @@ +{ + "type": "PropName" +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-002.xml b/crates/dav-proto/resources/requests/propfind-002.xml new file mode 100644 index 00000000..8d045183 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-002.xml @@ -0,0 +1,4 @@ + + + + diff --git a/crates/dav-proto/resources/requests/propfind-003.json b/crates/dav-proto/resources/requests/propfind-003.json new file mode 100644 index 00000000..0d980075 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-003.json @@ -0,0 +1,4 @@ +{ + "type": "AllProp", + "data": [] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-003.xml b/crates/dav-proto/resources/requests/propfind-003.xml new file mode 100644 index 00000000..b509470e --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-003.xml @@ -0,0 +1,4 @@ + + + + diff --git a/crates/dav-proto/resources/requests/propfind-004.json b/crates/dav-proto/resources/requests/propfind-004.json new file mode 100644 index 00000000..70bf3159 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-004.json @@ -0,0 +1,11 @@ +{ + "type": "AllProp", + "data": [ + { + "type": "WebDav", + "data": { + "type": "SupportedReportSet" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-004.xml b/crates/dav-proto/resources/requests/propfind-004.xml new file mode 100644 index 00000000..ed6f0ded --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-004.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/crates/dav-proto/resources/requests/propfind-005.json b/crates/dav-proto/resources/requests/propfind-005.json new file mode 100644 index 00000000..ac4ba7a0 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-005.json @@ -0,0 +1,11 @@ +{ + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "CurrentUserPrincipal" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-005.xml b/crates/dav-proto/resources/requests/propfind-005.xml new file mode 100644 index 00000000..b9b9185b --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-005.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/dav-proto/resources/requests/propfind-006.json b/crates/dav-proto/resources/requests/propfind-006.json new file mode 100644 index 00000000..566e803f --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-006.json @@ -0,0 +1,17 @@ +{ + "type": "Prop", + "data": [ + { + "type": "CalDav", + "data": { + "type": "CalendarHomeSet" + } + }, + { + "type": "WebDav", + "data": { + "type": "GroupMembership" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-006.xml b/crates/dav-proto/resources/requests/propfind-006.xml new file mode 100644 index 00000000..b5f399f0 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-006.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/dav-proto/resources/requests/propfind-007.json b/crates/dav-proto/resources/requests/propfind-007.json new file mode 100644 index 00000000..87a1bc2a --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-007.json @@ -0,0 +1,43 @@ +{ + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "CurrentUserPrivilegeSet" + } + }, + { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + }, + { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + { + "type": "DeadProperty", + "data": { + "name": "apple:calendar-color", + "attrs": null + } + }, + { + "type": "DeadProperty", + "data": { + "name": "cs:source", + "attrs": null + } + }, + { + "type": "CalDav", + "data": { + "type": "SupportedCalendarComponentSet" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-007.xml b/crates/dav-proto/resources/requests/propfind-007.xml new file mode 100644 index 00000000..0a39e9a1 --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-007.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/crates/dav-proto/resources/requests/propfind-008.json b/crates/dav-proto/resources/requests/propfind-008.json new file mode 100644 index 00000000..4b6757bd --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-008.json @@ -0,0 +1,30 @@ +{ + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "ResourceType" + } + }, + { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + { + "type": "DeadProperty", + "data": { + "name": "cs:getctag", + "attrs": null + } + }, + { + "type": "CalDav", + "data": { + "type": "SupportedCalendarComponentSet" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/propfind-008.xml b/crates/dav-proto/resources/requests/propfind-008.xml new file mode 100644 index 00000000..50545fed --- /dev/null +++ b/crates/dav-proto/resources/requests/propfind-008.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-001.json b/crates/dav-proto/resources/requests/report-001.json new file mode 100644 index 00000000..891205de --- /dev/null +++ b/crates/dav-proto/resources/requests/report-001.json @@ -0,0 +1,128 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [ + { + "component": "VCalendar", + "name": { + "type": "Version" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Summary" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Uid" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Dtstart" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Dtend" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Duration" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Rrule" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Rdate" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Exrule" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "Exdate" + }, + "no_value": false + }, + { + "component": "VEvent", + "name": { + "type": "RecurrenceId" + }, + "no_value": false + }, + { + "component": "VTimezone", + "name": null, + "no_value": false + } + ], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Component", + "comp": [ + "VCalendar", + "VEvent" + ], + "op": { + "type": "TimeRange", + "data": { + "start": 1136332800, + "end": 1136419200 + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-001.xml b/crates/dav-proto/resources/requests/report-001.xml new file mode 100644 index 00000000..29eea631 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-001.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-002.json b/crates/dav-proto/resources/requests/report-002.json new file mode 100644 index 00000000..bc9b0a46 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-002.json @@ -0,0 +1,42 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": { + "start": 1136246400, + "end": 1136419200 + }, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Component", + "comp": [ + "VCalendar", + "VEvent" + ], + "op": { + "type": "TimeRange", + "data": { + "start": 1136246400, + "end": 1136419200 + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-002.xml b/crates/dav-proto/resources/requests/report-002.xml new file mode 100644 index 00000000..796017d2 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-002.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/crates/dav-proto/resources/requests/report-003.json b/crates/dav-proto/resources/requests/report-003.json new file mode 100644 index 00000000..21dc8a00 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-003.json @@ -0,0 +1,42 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": { + "start": 1136246400, + "end": 1136419200 + }, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Component", + "comp": [ + "VCalendar", + "VEvent" + ], + "op": { + "type": "TimeRange", + "data": { + "start": 1136246400, + "end": 1136419200 + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-003.xml b/crates/dav-proto/resources/requests/report-003.xml new file mode 100644 index 00000000..1600fb46 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-003.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-004.json b/crates/dav-proto/resources/requests/report-004.json new file mode 100644 index 00000000..bd7ed8d3 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-004.json @@ -0,0 +1,42 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": { + "start": 1136160000, + "end": 1136246400 + } + } + } + } + ] + }, + "filters": [ + { + "type": "Component", + "comp": [ + "VCalendar", + "VFreebusy" + ], + "op": { + "type": "TimeRange", + "data": { + "start": 1136160000, + "end": 1136246400 + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-004.xml b/crates/dav-proto/resources/requests/report-004.xml new file mode 100644 index 00000000..cdd92b10 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-004.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-005.json b/crates/dav-proto/resources/requests/report-005.json new file mode 100644 index 00000000..c4552aca --- /dev/null +++ b/crates/dav-proto/resources/requests/report-005.json @@ -0,0 +1,46 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Component", + "comp": [ + "VCalendar", + "VTodo", + "VAlarm" + ], + "op": { + "type": "TimeRange", + "data": { + "start": 1136541600, + "end": 1136628000 + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-005.xml b/crates/dav-proto/resources/requests/report-005.xml new file mode 100644 index 00000000..d748d2a9 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-005.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-006.json b/crates/dav-proto/resources/requests/report-006.json new file mode 100644 index 00000000..a3da5973 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-006.json @@ -0,0 +1,51 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Property", + "comp": [ + "VCalendar", + "VEvent" + ], + "prop": { + "type": "Uid" + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "DC6C50A017428C5216A2F1CD@example.com", + "collation": "Octet", + "negate": false + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-006.xml b/crates/dav-proto/resources/requests/report-006.xml new file mode 100644 index 00000000..7818a482 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-006.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + DC6C50A017428C5216A2F1CD@example.com + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-007.json b/crates/dav-proto/resources/requests/report-007.json new file mode 100644 index 00000000..68b17c2a --- /dev/null +++ b/crates/dav-proto/resources/requests/report-007.json @@ -0,0 +1,72 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Property", + "comp": [ + "VCalendar", + "VEvent" + ], + "prop": { + "type": "Attendee" + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "mailto:lisa@example.com", + "collation": "AsciiCasemap", + "negate": false + } + } + }, + { + "type": "Parameter", + "comp": [ + "VCalendar", + "VEvent" + ], + "prop": { + "type": "Attendee" + }, + "param": "Partstat", + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "NEEDS-ACTION", + "collation": "AsciiCasemap", + "negate": false + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-007.xml b/crates/dav-proto/resources/requests/report-007.xml new file mode 100644 index 00000000..88983b5f --- /dev/null +++ b/crates/dav-proto/resources/requests/report-007.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + mailto:lisa@example.com + + NEEDS-ACTION + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-008.json b/crates/dav-proto/resources/requests/report-008.json new file mode 100644 index 00000000..1c6f4900 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-008.json @@ -0,0 +1,41 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Component", + "comp": [ + "VCalendar", + "VEvent" + ], + "op": { + "type": "Exists" + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-008.xml b/crates/dav-proto/resources/requests/report-008.xml new file mode 100644 index 00000000..2ea17131 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-008.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-009.json b/crates/dav-proto/resources/requests/report-009.json new file mode 100644 index 00000000..a6ee4296 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-009.json @@ -0,0 +1,64 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Property", + "comp": [ + "VCalendar", + "VTodo" + ], + "prop": { + "type": "Completed" + }, + "op": { + "type": "Undefined" + } + }, + { + "type": "Property", + "comp": [ + "VCalendar", + "VTodo" + ], + "prop": { + "type": "Status" + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "CANCELLED", + "collation": "AsciiCasemap", + "negate": true + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-009.xml b/crates/dav-proto/resources/requests/report-009.xml new file mode 100644 index 00000000..76c2e2ad --- /dev/null +++ b/crates/dav-proto/resources/requests/report-009.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + CANCELLED + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-010.json b/crates/dav-proto/resources/requests/report-010.json new file mode 100644 index 00000000..d7cabdfb --- /dev/null +++ b/crates/dav-proto/resources/requests/report-010.json @@ -0,0 +1,52 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [ + { + "type": "Property", + "comp": [ + "VCalendar", + "VEvent" + ], + "prop": { + "type": "Other", + "data": "X-ABC-GUID" + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "ABC", + "collation": "AsciiCasemap", + "negate": false + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-010.xml b/crates/dav-proto/resources/requests/report-010.xml new file mode 100644 index 00000000..bbeb447f --- /dev/null +++ b/crates/dav-proto/resources/requests/report-010.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + ABC + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-011.json b/crates/dav-proto/resources/requests/report-011.json new file mode 100644 index 00000000..6f5c8d09 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-011.json @@ -0,0 +1,33 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + } + ] + }, + "filters": [ + { + "type": "Component", + "comp": [ + "VCalendar", + "VEvent" + ], + "op": { + "type": "TimeRange", + "data": { + "start": 1094083200, + "end": 1094169600 + } + } + } + ], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-011.xml b/crates/dav-proto/resources/requests/report-011.xml new file mode 100644 index 00000000..8afce337 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-011.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-012.json b/crates/dav-proto/resources/requests/report-012.json new file mode 100644 index 00000000..72f4b644 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-012.json @@ -0,0 +1,30 @@ +{ + "type": "CalendarQuery", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "filters": [], + "timezone": { + "type": "None" + } +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-012.xml b/crates/dav-proto/resources/requests/report-012.xml new file mode 100644 index 00000000..65b3f959 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-012.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-013.json b/crates/dav-proto/resources/requests/report-013.json new file mode 100644 index 00000000..6ecabfd7 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-013.json @@ -0,0 +1,30 @@ +{ + "type": "CalendarMultiGet", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CalDav", + "data": { + "type": "CalendarData", + "data": { + "properties": [], + "expand": null, + "limit_recurrence": null, + "limit_freebusy": null + } + } + } + ] + }, + "hrefs": [ + "/bernard/work/abcd1.ics", + "/bernard/work/mtg1.ics" + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-013.xml b/crates/dav-proto/resources/requests/report-013.xml new file mode 100644 index 00000000..a87e867d --- /dev/null +++ b/crates/dav-proto/resources/requests/report-013.xml @@ -0,0 +1,10 @@ + + + + + + + /bernard/work/abcd1.ics + /bernard/work/mtg1.ics + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-014.json b/crates/dav-proto/resources/requests/report-014.json new file mode 100644 index 00000000..e2e2962b --- /dev/null +++ b/crates/dav-proto/resources/requests/report-014.json @@ -0,0 +1,86 @@ +{ + "type": "Addressbook", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CardDav", + "data": { + "type": "AddressData", + "data": [ + { + "group": null, + "name": { + "type": "Version" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Uid" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Nickname" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Email" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Fn" + }, + "no_value": false + } + ] + } + } + ] + }, + "filters": [ + { + "type": "AnyOf" + }, + { + "type": "AnyOf" + }, + { + "type": "Property", + "comp": null, + "prop": { + "name": { + "type": "Nickname" + }, + "group": null + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Equals", + "value": "me", + "collation": "UnicodeCasemap", + "negate": false + } + } + } + ], + "limit": null +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-014.xml b/crates/dav-proto/resources/requests/report-014.xml new file mode 100644 index 00000000..c367c61b --- /dev/null +++ b/crates/dav-proto/resources/requests/report-014.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + me + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-015.json b/crates/dav-proto/resources/requests/report-015.json new file mode 100644 index 00000000..1a6336ea --- /dev/null +++ b/crates/dav-proto/resources/requests/report-015.json @@ -0,0 +1,109 @@ +{ + "type": "Addressbook", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CardDav", + "data": { + "type": "AddressData", + "data": [ + { + "group": null, + "name": { + "type": "Version" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Uid" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Nickname" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Email" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Fn" + }, + "no_value": false + } + ] + } + } + ] + }, + "filters": [ + { + "type": "AnyOf" + }, + { + "type": "AnyOf" + }, + { + "type": "Property", + "comp": null, + "prop": { + "name": { + "type": "Fn" + }, + "group": null + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "daboo", + "collation": "UnicodeCasemap", + "negate": false + } + } + }, + { + "type": "AnyOf" + }, + { + "type": "Property", + "comp": null, + "prop": { + "name": { + "type": "Email" + }, + "group": null + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "daboo", + "collation": "UnicodeCasemap", + "negate": false + } + } + } + ], + "limit": null +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-015.xml b/crates/dav-proto/resources/requests/report-015.xml new file mode 100644 index 00000000..4374910c --- /dev/null +++ b/crates/dav-proto/resources/requests/report-015.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + daboo + + + daboo + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-016.json b/crates/dav-proto/resources/requests/report-016.json new file mode 100644 index 00000000..87bac76d --- /dev/null +++ b/crates/dav-proto/resources/requests/report-016.json @@ -0,0 +1,43 @@ +{ + "type": "Addressbook", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + } + ] + }, + "filters": [ + { + "type": "AnyOf" + }, + { + "type": "AnyOf" + }, + { + "type": "Property", + "comp": null, + "prop": { + "name": { + "type": "Fn" + }, + "group": null + }, + "op": { + "type": "TextMatch", + "data": { + "type": "TextMatch", + "match_type": "Contains", + "value": "daboo", + "collation": "UnicodeCasemap", + "negate": false + } + } + } + ], + "limit": 2 +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-016.xml b/crates/dav-proto/resources/requests/report-016.xml new file mode 100644 index 00000000..2cd16ff4 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-016.xml @@ -0,0 +1,17 @@ + + + + + + + + daboo + + + + 2 + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-017.json b/crates/dav-proto/resources/requests/report-017.json new file mode 100644 index 00000000..44ae3312 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-017.json @@ -0,0 +1,61 @@ +{ + "type": "AdressbookMultiGet", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CardDav", + "data": { + "type": "AddressData", + "data": [ + { + "group": null, + "name": { + "type": "Version" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Uid" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Nickname" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Email" + }, + "no_value": false + }, + { + "group": null, + "name": { + "type": "Fn" + }, + "no_value": false + } + ] + } + } + ] + }, + "hrefs": [ + "/home/bernard/addressbook/vcf102.vcf", + "/home/bernard/addressbook/vcf1.vcf" + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-017.xml b/crates/dav-proto/resources/requests/report-017.xml new file mode 100644 index 00000000..414e774c --- /dev/null +++ b/crates/dav-proto/resources/requests/report-017.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + /home/bernard/addressbook/vcf102.vcf + /home/bernard/addressbook/vcf1.vcf + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-018.json b/crates/dav-proto/resources/requests/report-018.json new file mode 100644 index 00000000..ab2b0c89 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-018.json @@ -0,0 +1,24 @@ +{ + "type": "AdressbookMultiGet", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + }, + { + "type": "CardDav", + "data": { + "type": "AddressData", + "data": [] + } + } + ] + }, + "hrefs": [ + "/home/bernard/addressbook/vcf3.vcf" + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-018.xml b/crates/dav-proto/resources/requests/report-018.xml new file mode 100644 index 00000000..8accfe59 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-018.xml @@ -0,0 +1,9 @@ + + + + + + + /home/bernard/addressbook/vcf3.vcf + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-019.json b/crates/dav-proto/resources/requests/report-019.json new file mode 100644 index 00000000..52402ea6 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-019.json @@ -0,0 +1,17 @@ +{ + "type": "SyncCollection", + "sync_token": "abc", + "properties": { + "type": "Prop", + "data": [ + { + "type": "WebDav", + "data": { + "type": "GetETag" + } + } + ] + }, + "level_inf": true, + "limit": 9 +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-019.xml b/crates/dav-proto/resources/requests/report-019.xml new file mode 100644 index 00000000..5c561f83 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-019.xml @@ -0,0 +1,10 @@ + + + + abc + infinite + 9 + + + + diff --git a/crates/dav-proto/resources/requests/report-020.json b/crates/dav-proto/resources/requests/report-020.json new file mode 100644 index 00000000..52a49643 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-020.json @@ -0,0 +1,11 @@ +{ + "type": "AclPrincipalPropSet", + "properties": [ + { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + } + ] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-020.xml b/crates/dav-proto/resources/requests/report-020.xml new file mode 100644 index 00000000..5b9b9b5f --- /dev/null +++ b/crates/dav-proto/resources/requests/report-020.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/dav-proto/resources/requests/report-021.json b/crates/dav-proto/resources/requests/report-021.json new file mode 100644 index 00000000..d12ead20 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-021.json @@ -0,0 +1,14 @@ +{ + "type": "PrincipalMatch", + "principal_properties": { + "Properties": [ + { + "type": "WebDav", + "data": { + "type": "Owner" + } + } + ] + }, + "properties": [] +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-021.xml b/crates/dav-proto/resources/requests/report-021.xml new file mode 100644 index 00000000..114793a8 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-021.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-022.json b/crates/dav-proto/resources/requests/report-022.json new file mode 100644 index 00000000..238f186d --- /dev/null +++ b/crates/dav-proto/resources/requests/report-022.json @@ -0,0 +1,61 @@ +{ + "type": "PrincipalPropertySearch", + "property_search": [ + { + "property": { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + "match_": "doE" + }, + { + "property": { + "type": "DeadProperty", + "data": { + "name": "B:title", + "attrs": null + } + }, + "match_": "Sales" + } + ], + "properties": [ + { + "type": "WebDav", + "data": { + "type": "DisplayName" + } + }, + { + "type": "DeadProperty", + "data": { + "name": "B:department", + "attrs": null + } + }, + { + "type": "DeadProperty", + "data": { + "name": "B:phone", + "attrs": null + } + }, + { + "type": "DeadProperty", + "data": { + "name": "B:office", + "attrs": null + } + }, + { + "type": "DeadProperty", + "data": { + "name": "B:salary", + "attrs": null + } + } + ], + "apply_to_principal_collection_set": false +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-022.xml b/crates/dav-proto/resources/requests/report-022.xml new file mode 100644 index 00000000..767edbc1 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-022.xml @@ -0,0 +1,22 @@ + + + + + + + doE + + + + + + Sales + + + + + + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-023.json b/crates/dav-proto/resources/requests/report-023.json new file mode 100644 index 00000000..b160fb60 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-023.json @@ -0,0 +1,3 @@ +{ + "type": "PrincipalSearchPropertySet" +} \ No newline at end of file diff --git a/crates/dav-proto/resources/requests/report-023.xml b/crates/dav-proto/resources/requests/report-023.xml new file mode 100644 index 00000000..f8b7d559 --- /dev/null +++ b/crates/dav-proto/resources/requests/report-023.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/001.xml b/crates/dav-proto/resources/responses/001.xml new file mode 100644 index 00000000..07731548 --- /dev/null +++ b/crates/dav-proto/resources/responses/001.xml @@ -0,0 +1,6 @@ + + + + /locked/ + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/002.xml b/crates/dav-proto/resources/responses/002.xml new file mode 100644 index 00000000..d840870d --- /dev/null +++ b/crates/dav-proto/resources/responses/002.xml @@ -0,0 +1,18 @@ + + + + http://www.example.com/file + + + Box type A + + HTTP/1.1 200 OK + + + Box type B + HTTP/1.1 403 Forbidden + The user does not have access to the DingALing property. + + + There has been an access violation error. + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/003.xml b/crates/dav-proto/resources/responses/003.xml new file mode 100644 index 00000000..fe755bfe --- /dev/null +++ b/crates/dav-proto/resources/responses/003.xml @@ -0,0 +1,50 @@ + + + + /container/ + + + 1997-12-02T01:42:21Z + Example collection + + + + + + + + + + + + + HTTP/1.1 200 OK + + + + /container/front.html + + + 1997-12-02T02:27:21Z + Example HTML resource + 4525 + text/html + "zzyzx" + Mon, 12 Jan 1998 09:25:56 GMT + + + + + + + + + + + + + HTTP/1.1 200 OK + + + diff --git a/crates/dav-proto/resources/responses/004.xml b/crates/dav-proto/resources/responses/004.xml new file mode 100644 index 00000000..5a74c55c --- /dev/null +++ b/crates/dav-proto/resources/responses/004.xml @@ -0,0 +1,8 @@ + + + + http://www.example.com/container/resource3 + HTTP/1.1 423 Locked + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/005.xml b/crates/dav-proto/resources/responses/005.xml new file mode 100644 index 00000000..03c199ea --- /dev/null +++ b/crates/dav-proto/resources/responses/005.xml @@ -0,0 +1,22 @@ + + + + + + + infinity + + http://example.org/~ejw/contact.html + + Second-604800 + + urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4 + + + http://example.com/workspace/webdav/proposal.doc + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/006.xml b/crates/dav-proto/resources/responses/006.xml new file mode 100644 index 00000000..ce54a793 --- /dev/null +++ b/crates/dav-proto/resources/responses/006.xml @@ -0,0 +1,27 @@ + + + + http://www.example.com/container/ + + + + + + + 0 + Jane Smith + Infinite + + urn:uuid:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76 + + + http://www.example.com/container/ + + + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/007.xml b/crates/dav-proto/resources/responses/007.xml new file mode 100644 index 00000000..d6f0c114 --- /dev/null +++ b/crates/dav-proto/resources/responses/007.xml @@ -0,0 +1,6 @@ + + + + /workspace/webdav/ + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/008.xml b/crates/dav-proto/resources/responses/008.xml new file mode 100644 index 00000000..b966e279 --- /dev/null +++ b/crates/dav-proto/resources/responses/008.xml @@ -0,0 +1,43 @@ + + + + http://cal.example.com/bernard/work/abcd2.ics + + + "fffff-abcd2" + + + HTTP/1.1 200 OK + + + + http://cal.example.com/bernard/work/abcd3.ics + + + "fffff-abcd3" + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/009.xml b/crates/dav-proto/resources/responses/009.xml new file mode 100644 index 00000000..7af87667 --- /dev/null +++ b/crates/dav-proto/resources/responses/009.xml @@ -0,0 +1,11 @@ + + + + + + + + + HTTP/1.1 200 OK + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/010.xml b/crates/dav-proto/resources/responses/010.xml new file mode 100644 index 00000000..40cecd2f --- /dev/null +++ b/crates/dav-proto/resources/responses/010.xml @@ -0,0 +1,20 @@ + + + + /home/bernard/addressbook/v102.vcf + + + "23ba4d-ff11fb" + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/011.xml b/crates/dav-proto/resources/responses/011.xml new file mode 100644 index 00000000..43e15699 --- /dev/null +++ b/crates/dav-proto/resources/responses/011.xml @@ -0,0 +1,27 @@ + + + + /home/bernard/addressbook/ + HTTP/1.1 507 Insufficient Storage + + Only two matching records were returned + + + /home/bernard/addressbook/v102.vcf + + + "23ba4d-ff11fb" + + HTTP/1.1 200 OK + + + + /home/bernard/addressbook/v104.vcf + + + "23ba4d-ff11fc" + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/012.xml b/crates/dav-proto/resources/responses/012.xml new file mode 100644 index 00000000..df7c55e1 --- /dev/null +++ b/crates/dav-proto/resources/responses/012.xml @@ -0,0 +1,12 @@ + + + + /a + + + + /c + + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/013.xml b/crates/dav-proto/resources/responses/013.xml new file mode 100644 index 00000000..ac089fde --- /dev/null +++ b/crates/dav-proto/resources/responses/013.xml @@ -0,0 +1,15 @@ + + + + + + + Full name + + + + + + Job title + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/014.xml b/crates/dav-proto/resources/responses/014.xml new file mode 100644 index 00000000..a8b5d897 --- /dev/null +++ b/crates/dav-proto/resources/responses/014.xml @@ -0,0 +1,55 @@ + + + + http://www.example.com/papers/ + + + + + + + Any operation + + + Read any object + + + + Read ACL + + + + + + + Read current user privilege set property + + + + + Write any object + + + + Write ACL + + + + Write properties + + + + Write resource content + + + + + Unlock resource + + + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/015.xml b/crates/dav-proto/resources/responses/015.xml new file mode 100644 index 00000000..0b3d38c8 --- /dev/null +++ b/crates/dav-proto/resources/responses/015.xml @@ -0,0 +1,14 @@ + + + + http://www.example.com/papers/ + + + + + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/016.xml b/crates/dav-proto/resources/responses/016.xml new file mode 100644 index 00000000..683b4aa9 --- /dev/null +++ b/crates/dav-proto/resources/responses/016.xml @@ -0,0 +1,29 @@ + + + http://www.example.com/papers/ + + + + + + http://www.example.com/acl/groups/maintainers + + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/017.xml b/crates/dav-proto/resources/responses/017.xml new file mode 100644 index 00000000..2e62b8f6 --- /dev/null +++ b/crates/dav-proto/resources/responses/017.xml @@ -0,0 +1,17 @@ + + + + http://www.example.com/papers/ + + + + + + + + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/018.xml b/crates/dav-proto/resources/responses/018.xml new file mode 100644 index 00000000..9577ac7b --- /dev/null +++ b/crates/dav-proto/resources/responses/018.xml @@ -0,0 +1,15 @@ + + + + http://www.example.com/papers/ + + + + http://www.example.com/acl/users/ + http://www.example.com/acl/groups/ + + + HTTP/1.1 200 OK + + + \ No newline at end of file diff --git a/crates/dav-proto/resources/responses/019.xml b/crates/dav-proto/resources/responses/019.xml new file mode 100644 index 00000000..e57fff27 --- /dev/null +++ b/crates/dav-proto/resources/responses/019.xml @@ -0,0 +1,81 @@ + + + + http://www.example.com/top/container/ + + + + http://www.example.com/users/gclemm + + + + + + Any operation + + + Read any object + + + + + Write any object + + + + + Read the ACL + + + + Write the ACL + + + + + + + + + + + http://www.example.com/users/esedlar + + + + + + + + + + http://www.example.com/groups/mrktng + + + + + + + + + + + + + + + + + + + + + http://www.example.com/top + + + + + HTTP/1.1 200 OK + + + diff --git a/crates/dav-proto/src/lib.rs b/crates/dav-proto/src/lib.rs new file mode 100644 index 00000000..572d2c6a --- /dev/null +++ b/crates/dav-proto/src/lib.rs @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod parser; +pub mod requests; +pub mod responses; +pub mod schema; + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct RequestHeaders<'x> { + pub uri: &'x str, + pub depth: Depth, + pub timeout: Timeout, + pub content_type: Option<&'x str>, + pub destination: Option<&'x str>, + pub lock_token: Option<&'x str>, + pub overwrite_fail: bool, + pub no_timezones: bool, + pub ret: Return, + pub depth_no_root: bool, + pub if_: Vec>, +} + +pub struct ResourceState> { + pub resource: Option, + pub etag: T, + pub state_token: T, +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub enum Return { + Minimal, + Representation, + #[default] + Default, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct If<'x> { + pub resource: Option<&'x str>, + pub list: Vec>, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Condition<'x> { + StateToken { is_not: bool, token: &'x str }, + ETag { is_not: bool, tag: &'x str }, + Exists { is_not: bool }, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum Timeout { + Infinite, + Second(u64), + #[default] + None, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum Depth { + Zero, + One, + Infinity, + #[default] + None, +} + +/* + + Allow: OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE + Allow: MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL + DAV: 1, 2, 3, access-control, extended-mkcol +calendar-no-timezone + + +TODO: + + +Implemented: + +RFC4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV) +RFC5689 - Extended MKCOL for Web Distributed Authoring and Versioning (WebDAV) +RFC6578 - Collection Synchronization for Web Distributed Authoring and Versioning (WebDAV) +RFC3744 - Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol +RFC4331 - Quota and Size Properties for Distributed Authoring and Versioning (DAV) Collections +RFC5397 - WebDAV Current Principal Extension +RFC8144 - Use of the Prefer Header Field in Web Distributed Authoring and Versioning (WebDAV) +RFC4791 - Calendaring Extensions to WebDAV (CalDAV) +RFC7809 - Calendaring Extensions to WebDAV (CalDAV) Time Zones by Reference +RFC6638 - Scheduling Extensions to CalDAV +RFC6352 - CardDAV vCard Extensions to Web Distributed Authoring and Versioning (WebDAV) +RFC6764 - Locating Services for Calendaring Extensions to WebDAV (CalDAV) and vCard Extensions to WebDAV (CardDAV) + +Out of scope: + +RFC5842 - Binding Extensions to Web Distributed Authoring and Versioning (WebDAV) +RFC4316 - Datatypes for Web Distributed Authoring and Versioning (WebDAV) Properties +RFC4709 - Mounting Web Distributed Authoring and Versioning (WebDAV) Servers +RFC3648 - Web Distributed Authoring and Versioning (WebDAV) Ordered Collections Protocol +RFC4437 - Web Distributed Authoring and Versioning (WebDAV) Redirect Reference Resources +RFC8607 - Calendaring Extensions to WebDAV (CalDAV) Managed Attachments +RFC5995 - Using POST to Add Members to Web Distributed Authoring and Versioning (WebDAV) Collections +RFC3253 - Versioning Extensions to WebDAV (Web Distributed Authoring and Versioning) +RFC5323 - Web Distributed Authoring and Versioning (WebDAV) SEARCH + + +*/ diff --git a/crates/dav-proto/src/parser/header.rs b/crates/dav-proto/src/parser/header.rs new file mode 100644 index 00000000..183dfbf1 --- /dev/null +++ b/crates/dav-proto/src/parser/header.rs @@ -0,0 +1,642 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{Condition, Depth, If, RequestHeaders, ResourceState, Return, Timeout}; + +impl<'x> RequestHeaders<'x> { + pub fn new(uri: &'x str) -> Self { + RequestHeaders { + uri, + ..Default::default() + } + } + + pub fn parse(&mut self, key: &str, value: &'x str) -> bool { + hashify::fnc_map_ignore_case!(key.as_bytes(), + "Depth" => { + if let Some(depth) = Depth::parse(value) { + self.depth = depth; + return true; + } + }, + "Destination" => { + self.destination = Some(value); + return true; + }, + "Lock-Token" => { + self.lock_token = Some(try_unwrap_coded_url(value)); + return true; + }, + "If" => { + let num = self.if_.len(); + self.parse_if(value); + return self.if_.len() != num; + }, + "If-Match" => { + let num = self.if_.len(); + self.parse_if_match(value, false); + return self.if_.len() != num; + }, + "If-None-Match" => { + let num = self.if_.len(); + self.parse_if_match(value, true); + return self.if_.len() != num; + }, + "Timeout" => { + let value = value.split_once(',').map(|(first, _)| first).unwrap_or(value).trim(); + if let Some(seconds) = value.strip_prefix("Second-") { + if let Ok(seconds) = seconds.parse() { + self.timeout = Timeout::Second(seconds); + return true; + } + } else if value == "Infinite" { + self.timeout = Timeout::Infinite; + return true; + } + }, + "Overwrite" => { + self.overwrite_fail = value == "F"; + return true; + }, + "CalDAV-Timezones" => { + self.no_timezones = value == "F"; + return true; + }, + "Prefer" => { + for value in value.split(&[',', ';']) { + match value.trim() { + "return=minimal" => self.ret = Return::Minimal, + "return=representation" => self.ret = Return::Representation, + "depth-noroot" => self.depth_no_root = true, + _ => {} + } + } + }, + "Content-Type" => { + let value = value.trim(); + if (2..=127).contains(&value.len()) { + self.content_type = Some(value); + } + return true; + }, + _ => {} + ); + + false + } + + pub fn base_uri(&self) -> Option<&'x str> { + // From a path ../dav/collection/account/.. + // returns ../dav/collection/account without the trailing slash + + let uri = self.uri.as_bytes(); + let mut found_dav = false; + let mut last_idx = 0; + let mut sep_count = 0; + + for (idx, ch) in uri.iter().enumerate() { + if *ch == b'/' { + if !found_dav { + found_dav = uri.get(idx + 1..idx + 5).is_some_and(|s| s == b"dav/"); + } else if found_dav { + if sep_count == 2 { + break; + } + sep_count += 1; + } + } + last_idx = idx; + } + + if sep_count == 2 { + uri.get(..last_idx + 1) + .map(|uri| std::str::from_utf8(uri).unwrap()) + } else { + None + } + } + + 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() + } + + pub fn eval_if_resources(&self) -> impl Iterator { + self.if_.iter().filter_map(|if_| if_.resource) + } + + pub fn eval_if(&self, resources: &[ResourceState]) -> bool + where + T: AsRef, + { + if self.if_.is_empty() { + return true; + } + + 'outer: for if_ in &self.if_ { + if if_.list.is_empty() { + continue; + } + + let (current_token, current_etag) = resources + .iter() + .find_map(|r| { + if if_.resource == r.resource.as_ref().map(|v| v.as_ref()) { + Some((r.state_token.as_ref(), r.etag.as_ref())) + } else { + None + } + }) + .unwrap_or_default(); + + for cond in if_.list.iter() { + match cond { + Condition::StateToken { is_not, token } => { + if !((current_token == *token) ^ is_not) { + continue 'outer; + } + } + Condition::ETag { is_not, tag } => { + if !((current_etag == *tag) ^ is_not) { + continue 'outer; + } + } + Condition::Exists { is_not } => { + if !((current_etag.is_empty()) ^ is_not) { + continue 'outer; + } + } + } + } + + return true; + } + + false + } + + fn parse_if(&mut self, value: &'x str) { + let value = value.as_bytes(); + let mut iter = value.iter().enumerate(); + let mut resource = None; + + while let Some((idx, ch)) = iter.next() { + match ch { + b'<' if resource.is_none() => { + for (to_idx, ch) in iter.by_ref() { + if *ch == b'>' { + resource = Some(std::str::from_utf8(&value[idx + 1..to_idx]).unwrap()); + break; + } + } + } + b'(' => { + let mut is_not = false; + let mut conditions = Vec::new(); + while let Some((idx, ch)) = iter.next() { + match ch { + b'N' => { + if matches!(iter.next(), Some((_, b'o'))) + && matches!(iter.next(), Some((_, b't'))) + { + is_not = true; + } else { + return; + } + } + b'<' | b'[' => { + let (stop_char, is_etag) = match ch { + b'<' => (b'>', false), + b'[' => (b']', true), + _ => unreachable!(), + }; + + for (to_idx, ch) in iter.by_ref() { + if *ch == stop_char { + let value = + std::str::from_utf8(&value[idx + 1..to_idx]).unwrap(); + let condition = if is_etag { + Condition::ETag { is_not, tag: value } + } else { + Condition::StateToken { + is_not, + token: value, + } + }; + conditions.push(condition); + is_not = false; + break; + } + } + } + b')' => { + self.if_.push(If { + resource: resource.take(), + list: conditions, + }); + break; + } + _ => { + if !ch.is_ascii_whitespace() { + return; + } + } + } + } + } + _ => { + if !ch.is_ascii_whitespace() { + return; + } + } + } + } + } + + pub fn parse_if_match(&mut self, value: &'x str, is_not: bool) { + if value == "*" { + self.if_.push(If { + resource: None, + list: vec![Condition::Exists { is_not }], + }); + } else if !is_not { + for etag in value.split(',') { + self.if_.push(If { + resource: None, + list: vec![Condition::ETag { + is_not, + tag: etag.trim(), + }], + }); + } + } else { + let mut etags = Vec::new(); + for etag in value.split(',') { + etags.push(Condition::ETag { + is_not, + tag: etag.trim(), + }); + } + self.if_.push(If { + resource: None, + list: etags, + }); + } + } +} + +impl Depth { + pub fn parse(value: &str) -> Option { + hashify::tiny_map!(value.as_bytes(), + "0" => Depth::Zero, + "1" => Depth::One, + "infinity" => Depth::Infinity, + ) + } +} + +fn try_unwrap_coded_url(url: &str) -> &str { + url.strip_prefix("<") + .and_then(|url| url.strip_suffix(">")) + .unwrap_or(url) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn base_uri() { + for (uri, expected_base) in [ + ( + "http://host/dav/collection/account/test/", + Some("http://host/dav/collection/account"), + ), + ( + "http://host/dav/collection/account/test", + Some("http://host/dav/collection/account"), + ), + ( + "http://host/dav/collection/account/", + Some("http://host/dav/collection/account"), + ), + ( + "http://host/dav/collection/account", + Some("http://host/dav/collection/account"), + ), + ( + "http://host/dev/dav/collection/account/test/", + Some("http://host/dev/dav/collection/account"), + ), + ( + "http://host/dev/dav/collection/account/test", + Some("http://host/dev/dav/collection/account"), + ), + ( + "http://host/dev/dav/collection/account/", + Some("http://host/dev/dav/collection/account"), + ), + ( + "http://host/dev/dav/collection/account", + Some("http://host/dev/dav/collection/account"), + ), + ( + "/dav/collection/account/test/", + Some("/dav/collection/account"), + ), + ( + "/dav/collection/account/test", + Some("/dav/collection/account"), + ), + ("/dav/collection/account/", Some("/dav/collection/account")), + ("/dav/collection/account", Some("/dav/collection/account")), + ] { + assert_eq!(RequestHeaders::new(uri).base_uri(), expected_base); + } + } + + #[test] + fn eval_if_header() { + let mut headers = RequestHeaders::default(); + assert!(headers.parse( + "If", + r#"( + ["I am an ETag"]) + (["I am another ETag"])"#, + )); + + assert!(headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + etag: "\"I am an ETag\"" + }])); + assert!(headers.eval_if(&[ResourceState { + resource: None, + state_token: "", + etag: "\"I am another ETag\"" + }])); + assert!(!headers.eval_if(&[ResourceState { + resource: None, + state_token: "", + etag: "\"Unknown ETag\"" + }])); + assert!(!headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + etag: "" + }])); + assert!(!headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + etag: "\"Other ETag\"" + }])); + assert!(!headers.eval_if(&[ResourceState { + resource: None, + state_token: "", + etag: "\"I am an ETag\"" + }])); + assert!(!headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:blah", + etag: "\"I am an ETag\"" + }])); + + assert!(headers.parse( + "If", + r#"(Not + )"#, + )); + assert!(headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092", + etag: "" + }])); + assert!(!headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + etag: "" + }])); + + assert!(headers.parse( + "If", + r#"() + (Not )"# + )); + assert!(headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + etag: "" + }])); + assert!(headers.eval_if(&[ResourceState { + resource: None, + state_token: "urn:other-token", + etag: "" + }])); + } + + #[test] + fn parse_headers() { + let mut headers = RequestHeaders::default(); + assert!(headers.parse("Depth", "0")); + assert_eq!(headers.depth, Depth::Zero); + + assert!(headers.parse("Destination", "/path/to/destination")); + assert_eq!(headers.destination, Some("/path/to/destination")); + + assert!(headers.parse("Lock-Token", "")); + assert_eq!(headers.lock_token, Some("urn:uuid:1234")); + + for (input, expected) in [ + ( + "()", + vec![If { + resource: "urn:uuid:1234".into(), + list: vec![Condition::StateToken { + is_not: false, + token: "urn:uuid:1234", + }], + }], + ), + ( + "<>(<>)", + vec![If { + resource: "".into(), + list: vec![Condition::StateToken { + is_not: false, + token: "", + }], + }], + ), + ( + r#"( + ["I am an ETag"]) + (["I am another ETag"])"#, + vec![ + If { + resource: None, + list: vec![ + Condition::StateToken { + is_not: false, + token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + }, + Condition::ETag { + is_not: false, + tag: "\"I am an ETag\"", + }, + ], + }, + If { + resource: None, + list: vec![Condition::ETag { + is_not: false, + tag: "\"I am another ETag\"", + }], + }, + ], + ), + ( + r#"(Not + )"#, + vec![If { + resource: None, + list: vec![ + Condition::StateToken { + is_not: true, + token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + }, + Condition::StateToken { + is_not: false, + token: "urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092", + }, + ], + }], + ), + ( + r#"() + (Not )"#, + vec![ + If { + resource: None, + list: vec![Condition::StateToken { + is_not: false, + token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + }], + }, + If { + resource: None, + list: vec![Condition::StateToken { + is_not: true, + token: "DAV:no-lock", + }], + }, + ], + ), + ( + r#" + ( + [W/"A weak ETag"]) (["strong ETag"])"#, + vec![ + If { + resource: "/resource1".into(), + list: vec![ + Condition::StateToken { + is_not: false, + token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + }, + Condition::ETag { + is_not: false, + tag: "W/\"A weak ETag\"", + }, + ], + }, + If { + resource: None, + list: vec![Condition::ETag { + is_not: false, + tag: "\"strong ETag\"", + }], + }, + ], + ), + ( + r#" + ()"#, + vec![If { + resource: "http://www.example.com/specs/".into(), + list: vec![Condition::StateToken { + is_not: false, + token: "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2", + }], + }], + ), + ( + r#" (["4217"])"#, + vec![If { + resource: "/specs/rfc2518.doc".into(), + list: vec![Condition::ETag { + is_not: false, + tag: "\"4217\"", + }], + }], + ), + ( + r#" (Not ["4217"])"#, + vec![If { + resource: "/specs/rfc2518.doc".into(), + list: vec![Condition::ETag { + is_not: true, + tag: "\"4217\"", + }], + }], + ), + ] { + assert!(headers.parse("If", input)); + assert_eq!(headers.if_, expected, "Failed for input: {}", input); + headers.if_.clear(); + } + + assert!(headers.parse("If-Match", "*")); + assert_eq!( + headers.if_, + vec![If { + resource: None, + list: vec![Condition::Exists { is_not: false }], + }] + ); + headers.if_.clear(); + + assert!(headers.parse("If-None-Match", "etag1, etag2")); + assert_eq!( + headers.if_, + vec![If { + resource: None, + list: vec![ + Condition::ETag { + is_not: true, + tag: "etag1", + }, + Condition::ETag { + is_not: true, + tag: "etag2", + } + ], + },] + ); + + assert!(headers.parse("Timeout", "Second-10")); + assert_eq!(headers.timeout, Timeout::Second(10)); + + assert!(headers.parse("Timeout", "Infinite, Second-4100000000")); + assert_eq!(headers.timeout, Timeout::Infinite); + + assert!(headers.parse("Overwrite", "F")); + assert!(headers.overwrite_fail); + } +} diff --git a/crates/dav-proto/src/parser/mod.rs b/crates/dav-proto/src/parser/mod.rs new file mode 100644 index 00000000..4ea82068 --- /dev/null +++ b/crates/dav-proto/src/parser/mod.rs @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::borrow::Cow; + +use quick_xml::events::BytesStart; +use tokenizer::Tokenizer; + +use crate::schema::{Element, NamedElement, Namespace}; + +pub mod header; +pub mod property; +pub mod tokenizer; + +#[derive(Debug, Clone)] +pub enum Error { + Xml(quick_xml::Error), + UnexpectedToken { + expected: Option>, + found: Token<'static>, + }, +} + +pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub enum Token<'x> { + ElementStart { + name: NamedElement, + raw: RawElement<'x>, + }, + ElementEnd, + Bytes(Cow<'x, [u8]>), + Text(Cow<'x, str>), + UnknownElement(RawElement<'x>), + Eof, +} + +#[derive(Debug, Clone)] +pub struct RawElement<'x>(pub BytesStart<'x>); + +pub trait DavParser: Sized { + fn parse(stream: &mut Tokenizer<'_>) -> Result; +} + +pub trait XmlValueParser: Sized { + fn parse_bytes(bytes: &[u8]) -> Option; + fn parse_str(text: &str) -> Option; +} + +impl NamedElement { + pub fn dav(element: Element) -> NamedElement { + NamedElement { + ns: Namespace::Dav, + element, + } + } + + pub fn caldav(element: Element) -> NamedElement { + NamedElement { + ns: Namespace::CalDav, + element, + } + } + + pub fn carddav(element: Element) -> NamedElement { + NamedElement { + ns: Namespace::CardDav, + element, + } + } +} + +impl Token<'_> { + pub fn into_owned(self) -> Token<'static> { + match self { + Token::ElementStart { name, raw } => Token::ElementStart { + name, + raw: RawElement(raw.0.into_owned()), + }, + Token::ElementEnd => Token::ElementEnd, + Token::Bytes(bytes) => Token::Bytes(bytes.into_owned().into()), + Token::Text(text) => Token::Text(text.into_owned().into()), + Token::UnknownElement(raw) => Token::UnknownElement(RawElement(raw.0.into_owned())), + Token::Eof => Token::Eof, + } + } + + pub fn into_unexpected(self) -> Error { + Error::UnexpectedToken { + expected: None, + found: self.into_owned(), + } + } +} + +#[cfg(test)] +impl PartialEq for Token<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Self::ElementStart { + name: l_name, + raw: l_raw, + }, + Self::ElementStart { + name: r_name, + raw: r_raw, + }, + ) => { + l_name == r_name + && l_raw + .0 + .attributes_raw() + .trim_ascii() + .eq_ignore_ascii_case(r_raw.0.attributes_raw().trim_ascii()) + } + (Self::Bytes(l0), Self::Bytes(r0)) => l0 == r0, + (Self::Text(l0), Self::Text(r0)) => l0 == r0, + (Self::UnknownElement(l0), Self::UnknownElement(r0)) => { + l0.0.as_ref().eq_ignore_ascii_case(r0.0.as_ref()) + } + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +impl NamedElement { + pub fn into_unexpected(self) -> Error { + Error::UnexpectedToken { + expected: None, + found: Token::ElementStart { + name: self, + raw: RawElement(BytesStart::new("")), + }, + } + } +} + +impl Default for RawElement<'_> { + fn default() -> Self { + RawElement(BytesStart::new("")) + } +} diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs new file mode 100644 index 00000000..15bb95f4 --- /dev/null +++ b/crates/dav-proto/src/parser/property.rs @@ -0,0 +1,683 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use calcard::{ + common::PartialDateTime, + icalendar::{ICalendar, ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, + vcard::{VCardParameterName, VCardProperty}, + Entry, Parser, +}; +use mail_parser::DateTime; + +use crate::schema::{ + property::{ + CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName, + Comp, DateRange, DavProperty, DavValue, ResourceType, WebDavProperty, + }, + request::{DavPropertyValue, DeadProperty, VCardPropertyWithGroup}, + response::List, + Attribute, AttributeValue, Element, NamedElement, Namespace, +}; + +use super::{tokenizer::Tokenizer, DavParser, RawElement, Token, XmlValueParser}; + +impl Tokenizer<'_> { + pub(crate) fn collect_properties(&mut self) -> crate::parser::Result> { + let mut elements = Vec::new(); + + loop { + match self.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::CalendarData, + }, + .. + } => { + elements.push(DavProperty::CalDav(CalDavProperty::CalendarData( + self.collect_calendar_data()?, + ))); + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CardDav, + element: Element::AddressData, + }, + .. + } => { + elements.push(DavProperty::CardDav(CardDavProperty::AddressData( + self.collect_address_data()?, + ))); + } + Token::ElementStart { name, .. } => { + if let Some(property) = DavProperty::from_element(name) { + elements.push(property); + } + self.expect_element_end()?; + } + Token::ElementEnd => { + break; + } + Token::UnknownElement(name) => { + elements.push(DavProperty::DeadProperty(name.into())); + self.expect_element_end()?; + } + token => return Err(token.into_unexpected()), + } + } + + Ok(elements) + } + + pub(crate) fn collect_calendar_data(&mut self) -> crate::parser::Result { + let mut depth = 1; + let mut data = CalendarData { + properties: Vec::with_capacity(4), + expand: None, + limit_recurrence: None, + limit_freebusy: None, + }; + let mut components: Vec = Vec::new(); + + loop { + match self.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::Allcomp, + }, + .. + } => { + self.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::Allprop, + }, + .. + } => { + if let Some(component) = components.last().copied() { + data.properties.push(CalDavPropertyName { + component: Some(component), + name: None, + no_value: false, + }); + } + self.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::Comp, + }, + raw, + } => { + depth += 1; + + for attribute in raw.attributes::() { + if let Attribute::Name(name) = attribute? { + components.push(name); + } + } + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::Prop, + }, + raw, + } => { + let mut name = None; + let mut no_value = false; + + for attribute in raw.attributes::() { + match attribute? { + Attribute::Name(name_) => { + name = Some(name_); + } + Attribute::NoValue(no_value_) => { + no_value = no_value_; + } + _ => {} + } + } + + if let Some(name) = name { + data.properties.push(CalDavPropertyName { + component: components.last().copied(), + name: Some(name), + no_value, + }); + } + + self.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::Expand, + }, + raw, + } => { + data.expand = Some(DateRange::from_raw(&raw)?); + self.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::LimitRecurrenceSet, + }, + raw, + } => { + data.limit_recurrence = Some(DateRange::from_raw(&raw)?); + self.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::LimitFreebusySet, + }, + raw, + } => { + data.limit_freebusy = Some(DateRange::from_raw(&raw)?); + self.expect_element_end()?; + } + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + if let Some(last_component) = components.pop() { + if last_component != ICalendarComponentType::VCalendar + && !matches!(data.properties.last(), Some(CalDavPropertyName { component: Some(component), .. }) if component == &last_component) + { + data.properties.push(CalDavPropertyName { + component: Some(last_component), + name: None, + no_value: false, + }); + } + } + } + Token::Eof => { + break; + } + token => return Err(token.into_unexpected()), + } + } + + Ok(data) + } + + pub(crate) fn collect_address_data( + &mut self, + ) -> crate::parser::Result> { + let mut items = Vec::with_capacity(4); + loop { + match self.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CardDav, + element: Element::Allprop, + }, + .. + } => { + self.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CardDav, + element: Element::Prop, + }, + raw, + } => { + let mut name = None; + let mut group = None; + let mut no_value = false; + + for attribute in raw.attributes::() { + match attribute? { + Attribute::Name(name_) => { + name = Some(name_.name); + group = name_.group; + } + Attribute::NoValue(no_value_) => { + no_value = no_value_; + } + _ => {} + } + } + + if let Some(name) = name { + items.push(CardDavPropertyName { + name, + group, + no_value, + }); + } + + self.expect_element_end()?; + } + Token::ElementEnd | Token::Eof => { + break; + } + token => return Err(token.into_unexpected()), + } + } + + Ok(items) + } +} + +impl Tokenizer<'_> { + pub(crate) fn collect_property_values( + &mut self, + ) -> crate::parser::Result> { + let mut elements = Vec::new(); + + loop { + match self.token()? { + Token::ElementStart { name, .. } => { + if let Some(property) = DavProperty::from_element(name) { + let value = match property { + DavProperty::WebDav(WebDavProperty::ResourceType) => { + DavValue::ResourceTypes(List(self.collect_elements()?)) + } + DavProperty::WebDav(WebDavProperty::CreationDate) => { + match self.parse_value::()? { + Some(Ok(value)) => DavValue::Timestamp(value.to_timestamp()), + Some(Err(value)) => DavValue::String(value), + None => DavValue::Null, + } + } + DavProperty::CalDav(CalDavProperty::CalendarTimezone) => { + match self.parse_value()? { + Some(Ok(value)) => DavValue::ICalendar(value), + Some(Err(value)) => DavValue::String(value), + None => DavValue::Null, + } + } + DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet) => { + let mut components = Vec::new(); + + loop { + match self.token()? { + Token::ElementStart { name, raw } => { + if name.ns == Namespace::CalDav + && name.element == Element::Comp + { + for component in + raw.attributes::() + { + if let Attribute::Name(name) = component? { + components.push(Comp(name)); + } + } + } + self.seek_element_end()?; + } + Token::UnknownElement(_) => { + // Ignore unknown elements + self.seek_element_end()?; + } + Token::ElementEnd | Token::Eof => { + break; + } + _ => {} + } + } + + DavValue::Components(List(components)) + } + DavProperty::CalDav( + CalDavProperty::MaxInstances + | CalDavProperty::MaxAttendeesPerInstance, + ) => match self.parse_value()? { + Some(Ok(value)) => DavValue::Uint64(value), + Some(Err(value)) => DavValue::String(value), + None => DavValue::Null, + }, + _ => self + .collect_string_value()? + .map(DavValue::String) + .unwrap_or(DavValue::Null), + }; + + elements.push(DavPropertyValue { property, value }); + } else { + // Ignore unknown elements + self.seek_element_end()?; + } + } + Token::ElementEnd | Token::Eof => { + break; + } + Token::UnknownElement(raw) => { + elements.push(DavPropertyValue { + property: DavProperty::DeadProperty(raw.into()), + value: DavValue::DeadProperty(DeadProperty::parse(self)?), + }); + } + token => return Err(token.into_unexpected()), + } + } + + Ok(elements) + } +} + +impl DateRange { + pub fn from_raw(raw: &RawElement<'_>) -> super::Result { + let mut range = DateRange { start: 0, end: 0 }; + + for attribute in raw.attributes::() { + match attribute? { + Attribute::Start(start) => { + range.start = start.0; + } + Attribute::End(end) => { + range.end = end.0; + } + _ => {} + } + } + + Ok(range) + } +} + +impl DavProperty { + pub(crate) fn from_element(element: NamedElement) -> Option { + match (element.ns, element.element) { + (Namespace::Dav, Element::Creationdate) => { + Some(DavProperty::WebDav(WebDavProperty::CreationDate)) + } + (Namespace::Dav, Element::Displayname) => { + Some(DavProperty::WebDav(WebDavProperty::DisplayName)) + } + (Namespace::Dav, Element::Getcontentlanguage) => { + Some(DavProperty::WebDav(WebDavProperty::GetContentLanguage)) + } + (Namespace::Dav, Element::Getcontentlength) => { + Some(DavProperty::WebDav(WebDavProperty::GetContentLength)) + } + (Namespace::Dav, Element::Getcontenttype) => { + Some(DavProperty::WebDav(WebDavProperty::GetContentType)) + } + (Namespace::Dav, Element::Getetag) => { + Some(DavProperty::WebDav(WebDavProperty::GetETag)) + } + (Namespace::Dav, Element::Getlastmodified) => { + Some(DavProperty::WebDav(WebDavProperty::GetLastModified)) + } + (Namespace::Dav, Element::Resourcetype) => { + Some(DavProperty::WebDav(WebDavProperty::ResourceType)) + } + (Namespace::Dav, Element::Lockdiscovery) => { + Some(DavProperty::WebDav(WebDavProperty::LockDiscovery)) + } + (Namespace::Dav, Element::Supportedlock) => { + Some(DavProperty::WebDav(WebDavProperty::SupportedLock)) + } + (Namespace::Dav, Element::CurrentUserPrincipal) => { + Some(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)) + } + (Namespace::Dav, Element::QuotaAvailableBytes) => { + Some(DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes)) + } + (Namespace::Dav, Element::QuotaUsedBytes) => { + Some(DavProperty::WebDav(WebDavProperty::QuotaUsedBytes)) + } + (Namespace::Dav, Element::SupportedReportSet) => { + Some(DavProperty::WebDav(WebDavProperty::SupportedReportSet)) + } + (Namespace::Dav, Element::SyncToken) => { + Some(DavProperty::WebDav(WebDavProperty::SyncToken)) + } + (Namespace::Dav, Element::AlternateUriSet) => { + Some(DavProperty::WebDav(WebDavProperty::AlternateURISet)) + } + (Namespace::Dav, Element::PrincipalUrl) => { + Some(DavProperty::WebDav(WebDavProperty::PrincipalURL)) + } + (Namespace::Dav, Element::GroupMemberSet) => { + Some(DavProperty::WebDav(WebDavProperty::GroupMemberSet)) + } + (Namespace::Dav, Element::GroupMembership) => { + Some(DavProperty::WebDav(WebDavProperty::GroupMembership)) + } + (Namespace::Dav, Element::Owner) => Some(DavProperty::WebDav(WebDavProperty::Owner)), + (Namespace::Dav, Element::Group) => Some(DavProperty::WebDav(WebDavProperty::Group)), + (Namespace::Dav, Element::SupportedPrivilegeSet) => { + Some(DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet)) + } + (Namespace::Dav, Element::CurrentUserPrivilegeSet) => { + Some(DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet)) + } + (Namespace::Dav, Element::Acl) => Some(DavProperty::WebDav(WebDavProperty::Acl)), + (Namespace::Dav, Element::AclRestrictions) => { + Some(DavProperty::WebDav(WebDavProperty::AclRestrictions)) + } + (Namespace::Dav, Element::InheritedAclSet) => { + Some(DavProperty::WebDav(WebDavProperty::InheritedAclSet)) + } + (Namespace::Dav, Element::PrincipalCollectionSet) => { + Some(DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet)) + } + (Namespace::CardDav, Element::AddressbookDescription) => Some(DavProperty::CardDav( + CardDavProperty::AddressbookDescription, + )), + (Namespace::CardDav, Element::SupportedAddressData) => { + Some(DavProperty::CardDav(CardDavProperty::SupportedAddressData)) + } + (Namespace::CardDav, Element::SupportedCollationSet) => { + Some(DavProperty::CardDav(CardDavProperty::SupportedCollationSet)) + } + (Namespace::CardDav, Element::AddressData) => Some(DavProperty::CardDav( + CardDavProperty::AddressData(Default::default()), + )), + (Namespace::CardDav, Element::MaxResourceSize) => { + Some(DavProperty::CardDav(CardDavProperty::MaxResourceSize)) + } + (Namespace::CalDav, Element::CalendarDescription) => { + Some(DavProperty::CalDav(CalDavProperty::CalendarDescription)) + } + (Namespace::CalDav, Element::CalendarTimezone) => { + Some(DavProperty::CalDav(CalDavProperty::CalendarTimezone)) + } + (Namespace::CalDav, Element::SupportedCalendarComponentSet) => Some( + DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet), + ), + (Namespace::CalDav, Element::SupportedCollationSet) => { + Some(DavProperty::CalDav(CalDavProperty::SupportedCollationSet)) + } + (Namespace::CalDav, Element::SupportedCalendarData) => { + Some(DavProperty::CalDav(CalDavProperty::SupportedCalendarData)) + } + (Namespace::CalDav, Element::MaxResourceSize) => { + Some(DavProperty::CalDav(CalDavProperty::MaxResourceSize)) + } + (Namespace::CalDav, Element::MinDateTime) => { + Some(DavProperty::CalDav(CalDavProperty::MinDateTime)) + } + (Namespace::CalDav, Element::MaxDateTime) => { + Some(DavProperty::CalDav(CalDavProperty::MaxDateTime)) + } + (Namespace::CalDav, Element::MaxInstances) => { + Some(DavProperty::CalDav(CalDavProperty::MaxInstances)) + } + (Namespace::CalDav, Element::MaxAttendeesPerInstance) => { + Some(DavProperty::CalDav(CalDavProperty::MaxAttendeesPerInstance)) + } + (Namespace::CalDav, Element::CalendarHomeSet) => { + Some(DavProperty::CalDav(CalDavProperty::CalendarHomeSet)) + } + (Namespace::CalDav, Element::CalendarData) => Some(DavProperty::CalDav( + CalDavProperty::CalendarData(Default::default()), + )), + (Namespace::CalDav, Element::TimezoneServiceSet) => { + Some(DavProperty::CalDav(CalDavProperty::TimezoneServiceSet)) + } + (Namespace::CalDav, Element::CalendarTimezoneId) => { + Some(DavProperty::CalDav(CalDavProperty::TimezoneId)) + } + _ => None, + } + } +} + +impl TryFrom for ResourceType { + type Error = (); + + fn try_from(value: NamedElement) -> Result { + match (value.ns, value.element) { + (Namespace::Dav, Element::Collection) => Ok(ResourceType::Collection), + (Namespace::Dav, Element::Principal) => Ok(ResourceType::Principal), + (Namespace::CardDav, Element::Addressbook) => Ok(ResourceType::AddressBook), + (Namespace::CalDav, Element::Calendar) => Ok(ResourceType::Calendar), + _ => Err(()), + } + } +} + +struct ICalendarDateTime(i64); + +impl AttributeValue for ICalendarDateTime { + fn from_str(s: &str) -> Option + where + Self: Sized, + { + let mut dt = PartialDateTime::default(); + dt.parse_timestamp(&mut s.as_bytes().iter().peekable()); + dt.to_timestamp().map(ICalendarDateTime) + } +} + +impl AttributeValue for ICalendarComponentType { + fn from_str(s: &str) -> Option + where + Self: Sized, + { + ICalendarComponentType::try_from(s.as_bytes()).ok() + } +} + +impl AttributeValue for ICalendarProperty { + fn from_str(s: &str) -> Option + where + Self: Sized, + { + ICalendarProperty::try_from(s.as_bytes()) + .unwrap_or_else(|_| ICalendarProperty::Other(s.to_string())) + .into() + } +} + +impl AttributeValue for ICalendarParameterName { + fn from_str(s: &str) -> Option + where + Self: Sized, + { + ICalendarParameterName::parse(s).into() + } +} + +impl AttributeValue for VCardPropertyWithGroup { + fn from_str(s: &str) -> Option + where + Self: Sized, + { + if let Some((group, s)) = s.split_once('.') { + VCardPropertyWithGroup { + name: VCardProperty::try_from(s.as_bytes()) + .unwrap_or_else(|_| VCardProperty::Other(s.to_string())), + group: group.to_string().into(), + } + .into() + } else { + VCardPropertyWithGroup { + name: VCardProperty::try_from(s.as_bytes()) + .unwrap_or_else(|_| VCardProperty::Other(s.to_string())), + group: None, + } + .into() + } + } +} + +impl AttributeValue for VCardParameterName { + fn from_str(s: &str) -> Option + where + Self: Sized, + { + VCardParameterName::parse(s).into() + } +} + +impl XmlValueParser for ICalendar { + fn parse_bytes(bytes: &[u8]) -> Option { + let text = String::from_utf8_lossy(bytes); + let mut parser = Parser::new(&text); + if let Entry::ICalendar(ical) = parser.entry() { + Some(ical) + } else { + None + } + } + + fn parse_str(text: &str) -> Option { + let mut parser = Parser::new(text); + if let Entry::ICalendar(ical) = parser.entry() { + Some(ical) + } else { + None + } + } +} + +impl XmlValueParser for u64 { + fn parse_bytes(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok()) + } + + fn parse_str(text: &str) -> Option { + text.parse().ok() + } +} + +impl XmlValueParser for u32 { + fn parse_bytes(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok()) + } + + fn parse_str(text: &str) -> Option { + text.parse().ok() + } +} + +impl XmlValueParser for DateTime { + fn parse_bytes(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes) + .ok() + .and_then(DateTime::parse_rfc3339) + } + + fn parse_str(text: &str) -> Option { + DateTime::parse_rfc3339(text) + } +} diff --git a/crates/dav-proto/src/parser/tokenizer.rs b/crates/dav-proto/src/parser/tokenizer.rs new file mode 100644 index 00000000..e7f2496c --- /dev/null +++ b/crates/dav-proto/src/parser/tokenizer.rs @@ -0,0 +1,466 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use quick_xml::{ + events::{attributes::AttrError, Event}, + name::ResolveResult, + NsReader, +}; + +use crate::schema::{Attribute, AttributeValue, Element, NamedElement, Namespace}; + +use super::{Error, RawElement, Token, XmlValueParser}; + +pub struct Tokenizer<'x> { + xml: NsReader<&'x [u8]>, + last_is_end: bool, +} + +impl<'x> Tokenizer<'x> { + pub fn new(input: &'x [u8]) -> Self { + let mut xml = NsReader::from_reader(input); + xml.config_mut().trim_text(true); + Self { + xml, + last_is_end: false, + } + } + + pub fn token(&mut self) -> super::Result { + loop { + if self.last_is_end { + self.last_is_end = false; + return Ok(Token::ElementEnd); + } + + let (resolve_result, event) = self.xml.read_resolved_event()?; + let tag = match event { + Event::Start(tag) => tag, + Event::Empty(tag) => { + self.last_is_end = true; + tag + } + Event::End(_) => { + return Ok(Token::ElementEnd); + } + Event::Text(text) if text.iter().any(|ch| !ch.is_ascii_whitespace()) => { + return text.unescape().map(Token::Text).map_err(Error::Xml); + } + Event::CData(bytes) => return Ok(Token::Bytes(bytes.into_inner())), + Event::Eof => return Ok(Token::Eof), + _ => { + continue; + } + }; + + // Parse element + let name = tag.name(); + match resolve_result { + ResolveResult::Bound(ns) if !ns.as_ref().is_empty() => { + if let (Some(ns), Some(element)) = ( + Namespace::try_parse(ns.as_ref()), + Element::try_parse(name.local_name().as_ref()).copied(), + ) { + return Ok(Token::ElementStart { + name: NamedElement { ns, element }, + raw: RawElement(tag), + }); + } else { + return Ok(Token::UnknownElement(RawElement(tag))); + } + } + _ => { + return Ok(Token::UnknownElement(RawElement(tag))); + } + } + } + } + + pub fn unwrap_named_element(&mut self) -> super::Result { + match self.token()? { + Token::ElementStart { name, .. } => Ok(name), + found => Err(Error::UnexpectedToken { + expected: None, + found: found.into_owned(), + }), + } + } + + pub fn expect_named_element(&mut self, expected: NamedElement) -> super::Result<()> { + match self.token()? { + Token::ElementStart { name, .. } if name == expected => Ok(()), + found => Err(Error::UnexpectedToken { + expected: Token::ElementStart { + name: expected, + raw: RawElement::default(), + } + .into(), + found: found.into_owned(), + }), + } + } + + pub fn expect_named_element_or_eof(&mut self, expected: NamedElement) -> super::Result { + match self.token()? { + Token::ElementStart { name, .. } if name == expected => Ok(true), + Token::Eof => Ok(false), + found => Err(Error::UnexpectedToken { + expected: Token::ElementStart { + name: expected, + raw: RawElement::default(), + } + .into(), + found: found.into_owned(), + }), + } + } + + pub fn expect_element_end(&mut self) -> super::Result<()> { + match self.token()? { + Token::ElementEnd => Ok(()), + found => Err(Error::UnexpectedToken { + expected: Token::ElementEnd.into(), + found: found.into_owned(), + }), + } + } + + pub fn seek_element_end(&mut self) -> super::Result<()> { + let mut depth = 1; + loop { + match self.token()? { + Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1, + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + return Ok(()); + } + } + Token::Eof => return Err(Token::Eof.into_unexpected()), + _ => {} + } + } + } + + pub fn collect_string_value(&mut self) -> super::Result> { + let mut depth = 1; + let mut value = None; + + loop { + match self.token()? { + Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1, + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + } + Token::Text(text) => { + value = Some(text.into_owned()); + } + Token::Bytes(bytes) => { + value = Some(String::from_utf8_lossy(&bytes).into_owned()); + } + Token::Eof => return Err(Token::Eof.into_unexpected()), + } + } + + Ok(value) + } + + pub fn parse_value(&mut self) -> super::Result>> { + let mut depth = 1; + let mut result: Option> = None; + + loop { + match self.token()? { + Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1, + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + } + Token::Text(text) => { + if let Some(value) = T::parse_str(&text) { + result = Some(Ok(value)); + } else { + result = Some(Err(text.into_owned())); + } + } + Token::Bytes(bytes) => { + if let Some(value) = T::parse_bytes(&bytes) { + result = Some(Ok(value)); + } else { + result = Some(Err(String::from_utf8_lossy(&bytes).into_owned())); + } + } + Token::Eof => return Err(Token::Eof.into_unexpected()), + } + } + + Ok(result) + } + + pub fn collect_elements(&mut self) -> super::Result> + where + T: TryFrom, + { + let mut elements = Vec::with_capacity(2); + let mut depth = 1; + + loop { + match self.token()? { + Token::ElementStart { name, .. } => { + if depth == 1 { + if let Ok(element) = T::try_from(name) { + elements.push(element); + } + } + + depth += 1; + } + Token::UnknownElement(_) => { + depth += 1; + } + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + } + Token::Eof => break, + _ => {} + } + } + Ok(elements) + } +} + +impl RawElement<'_> { + pub fn attributes( + &self, + ) -> impl Iterator>> + '_ { + self.0.attributes().filter_map(|attr| match attr { + Ok(attr) => match attr.unescape_value() { + Ok(value) => Attribute::from_param(attr.key.as_ref(), value).map(Ok), + Err(err) => Some(Err(err.into())), + }, + Err(err) => Some(Err(err.into())), + }) + } +} + +impl From for Error { + fn from(err: quick_xml::Error) -> Self { + Error::Xml(err) + } +} + +impl From for Error { + fn from(err: AttrError) -> Self { + Error::Xml(err.into()) + } +} + +#[cfg(test)] +mod tests { + + use std::borrow::Cow; + + use crate::schema::{Collation, MatchType}; + + use super::*; + + #[derive(Debug, PartialEq, Eq)] + pub enum TestToken<'x> { + ElementStart(NamedElement), + ElementEnd, + Attribute(Attribute), + Bytes(Cow<'x, [u8]>), + Text(Cow<'x, str>), + } + + #[test] + fn test_tokenizer() { + for (input, expected) in [ + ( + r#" + + + + + + + + + "#, + vec![ + TestToken::ElementStart(NamedElement { + ns: Namespace::CalDav, + element: Element::CalendarQuery, + }), + TestToken::ElementStart(NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + }), + TestToken::ElementStart(NamedElement { + ns: Namespace::Dav, + element: Element::Getetag, + }), + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CalDav, + element: Element::CalendarData, + }), + TestToken::ElementEnd, + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CalDav, + element: Element::Filter, + }), + TestToken::ElementStart(NamedElement { + ns: Namespace::CalDav, + element: Element::CompFilter, + }), + TestToken::Attribute(Attribute::Name("VCALENDAR".to_string())), + TestToken::ElementEnd, + TestToken::ElementEnd, + TestToken::ElementEnd, + ], + ), + ( + r#" + + + + + + + + + + + + + + me + + + "#, + vec![ + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::AddressbookQuery, + }), + TestToken::ElementStart(NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + }), + TestToken::ElementStart(NamedElement { + ns: Namespace::Dav, + element: Element::Getetag, + }), + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::AddressData, + }), + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::Prop, + }), + TestToken::Attribute(Attribute::Name("VERSION".to_string())), + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::Prop, + }), + TestToken::Attribute(Attribute::Name("UID".to_string())), + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::Prop, + }), + TestToken::Attribute(Attribute::Name("NICKNAME".to_string())), + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::Prop, + }), + TestToken::Attribute(Attribute::Name("EMAIL".to_string())), + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::Prop, + }), + TestToken::Attribute(Attribute::Name("FN".to_string())), + TestToken::ElementEnd, + TestToken::ElementEnd, + TestToken::ElementEnd, + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::Filter, + }), + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::PropFilter, + }), + TestToken::Attribute(Attribute::Name("NICKNAME".to_string())), + TestToken::ElementStart(NamedElement { + ns: Namespace::CardDav, + element: Element::TextMatch, + }), + TestToken::Attribute(Attribute::Collation(Collation::UnicodeCasemap)), + TestToken::Attribute(Attribute::MatchType(MatchType::Equals)), + TestToken::Text("me".into()), + TestToken::ElementEnd, + TestToken::ElementEnd, + TestToken::ElementEnd, + TestToken::ElementEnd, + ], + ), + ] { + let mut tokenizer = Tokenizer::new(input.as_bytes()); + let mut result = vec![]; + + loop { + match tokenizer.token() { + Ok(token) => match token { + Token::ElementStart { name, raw } => { + result.push(TestToken::ElementStart(name)); + for attr in raw.attributes::() { + result.push(TestToken::Attribute(attr.unwrap())); + } + } + Token::ElementEnd => { + result.push(TestToken::ElementEnd); + } + Token::Bytes(cow) => { + result.push(TestToken::Bytes(cow.into_owned().into())); + } + Token::Text(cow) => { + result.push(TestToken::Text(cow.into_owned().into())); + } + Token::UnknownElement(_) => { + //result.push(TestToken::UnknownElement(unknown_element)); + } + Token::Eof => break, + }, + Err(err) => { + panic!("Error: {:?}", err); + } + } + } + + assert_eq!(result, expected); + } + } +} diff --git a/crates/dav-proto/src/requests/acl.rs b/crates/dav-proto/src/requests/acl.rs new file mode 100644 index 00000000..2c9206cc --- /dev/null +++ b/crates/dav-proto/src/requests/acl.rs @@ -0,0 +1,452 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + parser::{tokenizer::Tokenizer, DavParser, Token}, + schema::{ + property::{DavValue, Privilege}, + request::{ + Acl, AclPrincipalPropSet, DavPropertyValue, PrincipalMatch, PrincipalMatchProperties, + PrincipalPropertySearch, PropertySearch, + }, + response::{Ace, GrantDeny, Href, List, Principal}, + Element, NamedElement, Namespace, + }, +}; + +impl DavParser for Acl { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + stream.expect_named_element(NamedElement::dav(Element::Acl))?; + + let mut acl = Acl { aces: vec![] }; + + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Ace, + }, + .. + } => { + acl.aces.push(Ace::parse(stream)?); + } + Token::ElementEnd => { + break; + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + other => { + return Err(other.into_unexpected()); + } + } + } + + Ok(acl) + } +} + +impl DavParser for Ace { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut ace = Ace { + principal: Principal::All, + invert: false, + grant_deny: GrantDeny::Grant(List(vec![])), + protected: false, + inherited: None, + }; + let mut depth = 1; + + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Principal, + }, + .. + } => { + ace.principal = Principal::parse(stream)?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Invert, + }, + .. + } if depth == 1 => { + ace.invert = true; + depth += 1; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Protected, + }, + .. + } if depth == 1 => { + ace.protected = true; + stream.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Inherited, + }, + .. + } if depth == 1 => { + stream.expect_named_element(NamedElement::dav(Element::Href))?; + ace.inherited = stream.collect_string_value()?.map(Href); + stream.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Grant, + }, + .. + } if depth == 1 => { + ace.grant_deny = GrantDeny::Grant(List(stream.collect_privileges()?)); + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Deny, + }, + .. + } if depth == 1 => { + ace.grant_deny = GrantDeny::Deny(List(stream.collect_privileges()?)); + } + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + other => { + return Err(other.into_unexpected()); + } + } + } + + Ok(ace) + } +} + +impl DavParser for Principal { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let result = match stream.unwrap_named_element()? { + NamedElement { + ns: Namespace::Dav, + element: Element::Href, + } => Principal::Href(Href(stream.collect_string_value()?.unwrap_or_default())), + NamedElement { + ns: Namespace::Dav, + element: Element::All, + } => { + stream.expect_element_end()?; + Principal::All + } + NamedElement { + ns: Namespace::Dav, + element: Element::Authenticated, + } => { + stream.expect_element_end()?; + Principal::Authenticated + } + NamedElement { + ns: Namespace::Dav, + element: Element::Unauthenticated, + } => { + stream.expect_element_end()?; + Principal::Unauthenticated + } + NamedElement { + ns: Namespace::Dav, + element: Element::Property, + } => { + let property = stream.collect_properties()?; + Principal::Property(List( + property + .into_iter() + .map(|prop| DavPropertyValue::new(prop, DavValue::Null)) + .collect(), + )) + } + NamedElement { + ns: Namespace::Dav, + element: Element::Self_, + } => { + stream.expect_element_end()?; + Principal::Self_ + } + other => return Err(other.into_unexpected()), + }; + stream.expect_element_end()?; + Ok(result) + } +} + +impl Tokenizer<'_> { + pub fn collect_privileges(&mut self) -> crate::parser::Result> { + let mut privileges = Vec::new(); + let mut depth = 1; + + loop { + match self.token()? { + Token::ElementStart { name, .. } => { + if let Some(privilege) = Privilege::from_element(name) { + privileges.push(privilege); + self.expect_element_end()?; + } else { + depth += 1; + } + } + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + } + Token::UnknownElement(_) => { + self.seek_element_end()?; + } + other => { + return Err(other.into_unexpected()); + } + } + } + + Ok(privileges) + } +} + +impl Privilege { + pub fn from_element(element: NamedElement) -> Option { + match (element.ns, element.element) { + (Namespace::Dav, Element::Read) => Some(Privilege::Read), + (Namespace::Dav, Element::Write) => Some(Privilege::Write), + (Namespace::Dav, Element::WriteProperties) => Some(Privilege::WriteProperties), + (Namespace::Dav, Element::WriteContent) => Some(Privilege::WriteContent), + (Namespace::Dav, Element::Unlock) => Some(Privilege::Unlock), + (Namespace::Dav, Element::ReadAcl) => Some(Privilege::ReadAcl), + (Namespace::Dav, Element::ReadCurrentUserPrivilegeSet) => { + Some(Privilege::ReadCurrentUserPrivilegeSet) + } + (Namespace::Dav, Element::WriteAcl) => Some(Privilege::WriteAcl), + (Namespace::Dav, Element::Bind) => Some(Privilege::Bind), + (Namespace::Dav, Element::Unbind) => Some(Privilege::Unbind), + (Namespace::Dav, Element::All) => Some(Privilege::All), + (Namespace::CalDav, Element::ReadFreeBusy) => Some(Privilege::ReadFreeBusy), + _ => None, + } + } +} + +impl DavParser for AclPrincipalPropSet { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut acps = AclPrincipalPropSet { properties: vec![] }; + + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + }, + .. + } => { + acps.properties.extend(stream.collect_properties()?); + } + Token::ElementEnd => { + break; + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + other => { + return Err(other.into_unexpected()); + } + } + } + + Ok(acps) + } +} + +impl DavParser for PrincipalMatch { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut pm = PrincipalMatch { + principal_properties: PrincipalMatchProperties::Self_, + properties: vec![], + }; + + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::PrincipalProperty, + }, + .. + } => { + pm.principal_properties = + PrincipalMatchProperties::Properties(stream.collect_properties()?); + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Self_, + }, + .. + } => { + pm.principal_properties = PrincipalMatchProperties::Self_; + stream.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + }, + .. + } => { + pm.properties = stream.collect_properties()?; + } + Token::ElementEnd => { + break; + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + other => { + return Err(other.into_unexpected()); + } + } + } + + Ok(pm) + } +} + +impl DavParser for PrincipalPropertySearch { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut pps = PrincipalPropertySearch { + property_search: vec![], + properties: vec![], + apply_to_principal_collection_set: false, + }; + + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::PropertySearch, + }, + .. + } => { + if let Some(prop) = PropertySearch::parse(stream)? { + pps.property_search.push(prop); + } + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + }, + .. + } => { + pps.properties = stream.collect_properties()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::ApplyToPrincipalCollectionSet, + }, + .. + } => { + stream.expect_element_end()?; + pps.apply_to_principal_collection_set = true; + } + Token::ElementEnd => { + break; + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + other => { + return Err(other.into_unexpected()); + } + } + } + + Ok(pps) + } +} + +impl PropertySearch { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result> { + let mut property = None; + let mut match_ = None; + + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + }, + .. + } => { + property = stream.collect_properties()?.into_iter().next(); + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Match, + }, + .. + } => { + match_ = stream.collect_string_value()?; + } + Token::ElementEnd => { + break; + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + other => { + return Err(other.into_unexpected()); + } + } + } + + Ok(property.map(|property| PropertySearch { + property, + match_: match_.unwrap_or_default(), + })) + } +} diff --git a/crates/dav-proto/src/requests/lockinfo.rs b/crates/dav-proto/src/requests/lockinfo.rs new file mode 100644 index 00000000..ebcc04d8 --- /dev/null +++ b/crates/dav-proto/src/requests/lockinfo.rs @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + parser::{tokenizer::Tokenizer, DavParser, Token}, + schema::{ + property::{LockScope, LockType}, + request::{DeadProperty, LockInfo}, + Element, NamedElement, Namespace, + }, +}; + +impl DavParser for LockInfo { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut lockinfo = LockInfo { + lock_scope: LockScope::Exclusive, + lock_type: LockType::Write, + owner: None, + }; + + if stream.expect_named_element_or_eof(NamedElement::dav(Element::Lockinfo))? { + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Lockscope, + }, + .. + } => { + lockinfo.lock_scope = LockScope::parse(stream)?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Locktype, + }, + .. + } => { + lockinfo.lock_type = LockType::parse(stream)?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Owner, + }, + .. + } => { + lockinfo.owner = Some(DeadProperty::parse(stream)?); + } + Token::ElementEnd | Token::Eof => { + break; + } + other => { + return Err(other.into_unexpected()); + } + } + } + } + + Ok(lockinfo) + } +} + +impl DavParser for LockScope { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + match stream.unwrap_named_element()? { + NamedElement { + ns: Namespace::Dav, + element: Element::Exclusive, + } => { + stream.expect_element_end()?; + stream.expect_element_end()?; + Ok(LockScope::Exclusive) + } + NamedElement { + ns: Namespace::Dav, + element: Element::Shared, + } => { + stream.expect_element_end()?; + stream.expect_element_end()?; + Ok(LockScope::Shared) + } + other => Err(other.into_unexpected()), + } + } +} + +impl DavParser for LockType { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + match stream.unwrap_named_element()? { + NamedElement { + ns: Namespace::Dav, + element: Element::Write, + } => { + stream.expect_element_end()?; + stream.expect_element_end()?; + Ok(LockType::Write) + } + other => Err(other.into_unexpected()), + } + } +} diff --git a/crates/dav-proto/src/requests/mkcol.rs b/crates/dav-proto/src/requests/mkcol.rs new file mode 100644 index 00000000..cc1498f1 --- /dev/null +++ b/crates/dav-proto/src/requests/mkcol.rs @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + parser::{tokenizer::Tokenizer, DavParser, Token}, + schema::{request::MkCol, Element, NamedElement, Namespace}, +}; + +impl DavParser for MkCol { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut mkcol = MkCol { + is_mkcalendar: false, + props: Vec::new(), + }; + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Mkcol, + }, + .. + } => {} + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::Mkcalendar, + }, + .. + } => { + mkcol.is_mkcalendar = true; + } + Token::Eof => { + return Ok(mkcol); + } + other => return Err(other.into_unexpected()), + }; + + stream.expect_named_element(NamedElement::dav(Element::Set))?; + stream.expect_named_element(NamedElement::dav(Element::Prop))?; + mkcol.props = stream.collect_property_values()?; + + Ok(mkcol) + } +} diff --git a/crates/dav-proto/src/requests/mod.rs b/crates/dav-proto/src/requests/mod.rs new file mode 100644 index 00000000..7807a508 --- /dev/null +++ b/crates/dav-proto/src/requests/mod.rs @@ -0,0 +1,215 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + parser::{tokenizer::Tokenizer, DavParser, RawElement, Token}, + schema::request::{ + ArchivedDeadElementTag, ArchivedDeadProperty, ArchivedDeadPropertyTag, DeadElementTag, + DeadProperty, DeadPropertyTag, + }, +}; + +pub mod acl; +pub mod lockinfo; +pub mod mkcol; +pub mod propertyupdate; +pub mod propfind; +pub mod report; + +impl DavParser for DeadProperty { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut depth = 1; + let mut items = DeadProperty::default(); + + loop { + match stream.token()? { + Token::ElementStart { raw, .. } | Token::UnknownElement(raw) => { + items.0.push(DeadPropertyTag::ElementStart(raw.into())); + depth += 1; + } + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + items.0.push(DeadPropertyTag::ElementEnd); + } + Token::Text(text) => { + items.0.push(DeadPropertyTag::Text(text.into_owned())); + } + Token::Bytes(bytes) => { + items.0.push(DeadPropertyTag::Text( + String::from_utf8_lossy(&bytes).into_owned(), + )); + } + Token::Eof => { + break; + } + } + } + + Ok(items) + } +} + +impl DeadProperty { + pub fn remove_element(&mut self, element: &DeadElementTag) { + let mut depth = 0; + let mut remove = false; + self.0.retain(|item| match item { + DeadPropertyTag::ElementStart(tag) => { + if depth == 0 && !remove && tag == element { + remove = true; + } + depth += 1; + + !remove + } + DeadPropertyTag::ElementEnd => { + depth -= 1; + if remove && depth == 0 { + remove = false; + false + } else { + !remove + } + } + _ => !remove, + }); + } + + pub fn add_element(&mut self, element: DeadElementTag, values: Vec) { + self.0.push(DeadPropertyTag::ElementStart(element)); + self.0.extend(values); + self.0.push(DeadPropertyTag::ElementEnd); + } + + pub fn size(&self) -> usize { + let mut size = 0; + for item in &self.0 { + match item { + DeadPropertyTag::ElementStart(tag) => { + size += tag.size(); + } + DeadPropertyTag::ElementEnd => { + size += 1; + } + DeadPropertyTag::Text(text) => { + size += text.len(); + } + } + } + size + } +} + +impl ArchivedDeadProperty { + pub fn size(&self) -> usize { + let mut size = 0; + for item in self.0.iter() { + match item { + ArchivedDeadPropertyTag::ElementStart(tag) => { + size += tag.size(); + } + ArchivedDeadPropertyTag::ElementEnd => { + size += 1; + } + ArchivedDeadPropertyTag::Text(text) => { + size += text.len(); + } + } + } + size + } +} + +impl DeadElementTag { + pub fn size(&self) -> usize { + self.name.len() + self.attrs.as_ref().map_or(0, |attrs| attrs.len()) + } +} + +impl ArchivedDeadElementTag { + pub fn size(&self) -> usize { + self.name.len() + self.attrs.as_ref().map_or(0, |attrs| attrs.len()) + } +} + +impl From> for DeadElementTag { + fn from(raw: RawElement<'_>) -> Self { + let name = String::from_utf8_lossy(raw.0.name().as_ref().trim_ascii()).into_owned(); + let attr = raw.0.attributes_raw().trim_ascii(); + + DeadElementTag { + name, + attrs: (!attr.is_empty()).then(|| String::from_utf8_lossy(attr).into_owned()), + } + } +} + +impl Default for DeadProperty { + fn default() -> Self { + DeadProperty(Vec::with_capacity(4)) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + parser::{tokenizer::Tokenizer, DavParser}, + schema::request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report}, + }; + + #[test] + fn parse_requests() { + for entry in std::fs::read_dir("resources/requests").unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + if path.extension().map(|ext| ext == "xml").unwrap_or(false) { + println!("Parsing: {:?}", path); + let filename = path.file_name().unwrap().to_str().unwrap(); + let xml = std::fs::read_to_string(&path).unwrap(); + let mut tokenizer = Tokenizer::new(xml.as_bytes()); + + let json_path = path.with_extension("json"); + let json_output = match filename.split_once('-').unwrap().0 { + "propfind" => { + serde_json::to_string_pretty(&PropFind::parse(&mut tokenizer).unwrap()) + .unwrap() + } + "propertyupdate" => serde_json::to_string_pretty( + &PropertyUpdate::parse(&mut tokenizer).unwrap(), + ) + .unwrap(), + "mkcol" => serde_json::to_string_pretty(&MkCol::parse(&mut tokenizer).unwrap()) + .unwrap(), + "lockinfo" => { + serde_json::to_string_pretty(&LockInfo::parse(&mut tokenizer).unwrap()) + .unwrap() + } + "report" => { + serde_json::to_string_pretty(&Report::parse(&mut tokenizer).unwrap()) + .unwrap() + } + "acl" => { + serde_json::to_string_pretty(&Acl::parse(&mut tokenizer).unwrap()).unwrap() + } + _ => { + panic!("Unknown method: {}", filename); + } + }; + + /*if json_path.exists() { + let expected = std::fs::read_to_string(json_path).unwrap(); + assert_eq!(json_output, expected); + } else {*/ + std::fs::write(json_path, json_output).unwrap(); + //} + } + } + } +} diff --git a/crates/dav-proto/src/requests/propertyupdate.rs b/crates/dav-proto/src/requests/propertyupdate.rs new file mode 100644 index 00000000..c7a5c0cf --- /dev/null +++ b/crates/dav-proto/src/requests/propertyupdate.rs @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + parser::{tokenizer::Tokenizer, DavParser, Token}, + schema::{request::PropertyUpdate, Element, NamedElement, Namespace}, +}; + +impl DavParser for PropertyUpdate { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + stream.expect_named_element(NamedElement::dav(Element::Propertyupdate))?; + let mut update = PropertyUpdate { + set: Vec::with_capacity(4), + remove: Vec::with_capacity(4), + }; + + loop { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Set, + }, + .. + } => { + stream.expect_named_element(NamedElement::dav(Element::Prop))?; + update.set = stream.collect_property_values()?; + stream.expect_element_end()?; + } + Token::ElementStart { + name: + NamedElement { + ns: Namespace::Dav, + element: Element::Remove, + }, + .. + } => { + stream.expect_named_element(NamedElement::dav(Element::Prop))?; + update.remove = stream.collect_properties()?; + stream.expect_element_end()?; + } + Token::ElementEnd | Token::Eof => { + break; + } + Token::UnknownElement(_) => { + // Ignore unknown elements + stream.seek_element_end()?; + } + token => return Err(token.into_unexpected()), + } + } + + Ok(update) + } +} diff --git a/crates/dav-proto/src/requests/propfind.rs b/crates/dav-proto/src/requests/propfind.rs new file mode 100644 index 00000000..fde66dd2 --- /dev/null +++ b/crates/dav-proto/src/requests/propfind.rs @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + parser::{tokenizer::Tokenizer, DavParser, Token}, + schema::{request::PropFind, Element, NamedElement, Namespace}, +}; + +impl DavParser for PropFind { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + if stream.expect_named_element_or_eof(NamedElement::dav(Element::Propfind))? { + match stream.unwrap_named_element()? { + NamedElement { + ns: Namespace::Dav, + element: Element::Propname, + } => Ok(PropFind::PropName), + NamedElement { + ns: Namespace::Dav, + element: Element::Allprop, + } => { + stream.expect_element_end()?; + if matches!( + stream.token()?, + Token::ElementStart { + name: NamedElement { + ns: Namespace::Dav, + element: Element::Include + }, + .. + } + ) { + stream.collect_properties().map(PropFind::AllProp) + } else { + Ok(PropFind::AllProp(vec![])) + } + } + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + } => stream.collect_properties().map(PropFind::Prop), + element => Err(element.into_unexpected()), + } + } else { + Ok(PropFind::AllProp(vec![])) + } + } +} diff --git a/crates/dav-proto/src/requests/report.rs b/crates/dav-proto/src/requests/report.rs new file mode 100644 index 00000000..47294d81 --- /dev/null +++ b/crates/dav-proto/src/requests/report.rs @@ -0,0 +1,574 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use calcard::{ + icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, + vcard::VCardParameterName, +}; + +use crate::{ + parser::{tokenizer::Tokenizer, DavParser, RawElement, Token, XmlValueParser}, + schema::{ + property::DateRange, + request::{ + AclPrincipalPropSet, AddressbookQuery, CalendarQuery, Filter, FilterOp, FreeBusyQuery, + MultiGet, PrincipalMatch, PrincipalPropertySearch, PropFind, Report, SyncCollection, + TextMatch, Timezone, VCardPropertyWithGroup, + }, + Attribute, Collation, Element, MatchType, NamedElement, Namespace, + }, +}; + +impl DavParser for Report { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + match stream.unwrap_named_element()? { + NamedElement { + ns: Namespace::CalDav, + element: Element::CalendarQuery, + } => CalendarQuery::parse(stream).map(Report::CalendarQuery), + NamedElement { + ns: Namespace::CalDav, + element: Element::FreeBusyQuery, + } => FreeBusyQuery::parse(stream).map(Report::FreeBusyQuery), + NamedElement { + ns: Namespace::CalDav, + element: Element::CalendarMultiget, + } => MultiGet::parse(stream).map(Report::CalendarMultiGet), + NamedElement { + ns: Namespace::CardDav, + element: Element::AddressbookQuery, + } => AddressbookQuery::parse(stream).map(Report::Addressbook), + NamedElement { + ns: Namespace::CardDav, + element: Element::AddressbookMultiget, + } => MultiGet::parse(stream).map(Report::AddressbookMultiGet), + NamedElement { + ns: Namespace::Dav, + element: Element::SyncCollection, + } => SyncCollection::parse(stream).map(Report::SyncCollection), + NamedElement { + ns: Namespace::Dav, + element: Element::AclPrincipalPropSet, + } => AclPrincipalPropSet::parse(stream).map(Report::AclPrincipalPropSet), + NamedElement { + ns: Namespace::Dav, + element: Element::PrincipalMatch, + } => PrincipalMatch::parse(stream).map(Report::PrincipalMatch), + NamedElement { + ns: Namespace::Dav, + element: Element::PrincipalPropertySearch, + } => PrincipalPropertySearch::parse(stream).map(Report::PrincipalPropertySearch), + NamedElement { + ns: Namespace::Dav, + element: Element::PrincipalSearchPropertySet, + } => stream + .expect_element_end() + .map(|_| Report::PrincipalSearchPropertySet), + other => Err(other.into_unexpected()), + } + } +} + +impl DavParser for CalendarQuery { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut cq = CalendarQuery { + properties: PropFind::AllProp(vec![]), + filters: vec![], + timezone: Timezone::None, + }; + let mut depth = 1; + let mut components = Vec::with_capacity(3); + let mut property = None; + let mut parameter = None; + + loop { + match stream.token()? { + Token::ElementStart { name, raw } => match name { + NamedElement { + ns: Namespace::Dav, + element: Element::Propname, + } if depth == 1 => { + cq.properties = PropFind::PropName; + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::Allprop, + } if depth == 1 => { + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + } if depth == 1 => { + cq.properties = PropFind::Prop(stream.collect_properties()?); + } + NamedElement { + ns: Namespace::CalDav, + element: Element::Filter, + } if depth == 1 => { + depth += 1; + } + NamedElement { + ns: Namespace::CalDav, + element: Element::Timezone, + } if depth == 1 => { + cq.timezone = + Timezone::Name(stream.collect_string_value()?.unwrap_or_default()); + } + NamedElement { + ns: Namespace::CalDav, + element: Element::TimezoneId, + } if depth == 1 => { + cq.timezone = + Timezone::Id(stream.collect_string_value()?.unwrap_or_default()); + } + NamedElement { + ns: Namespace::CalDav, + element: Element::CompFilter, + } if depth >= 2 => { + for attribute in raw.attributes::() { + if let Attribute::Name(name) = attribute? { + components.push((name, depth)); + } + } + depth += 1; + } + + NamedElement { + ns: Namespace::CalDav, + element: Element::PropFilter, + } if depth >= 3 => { + for attribute in raw.attributes::() { + if let Attribute::Name(name) = attribute? { + property = Some(name); + } + } + depth += 1; + } + NamedElement { + ns: Namespace::CalDav, + element: Element::ParamFilter, + } if depth >= 4 => { + for attribute in raw.attributes::() { + if let Attribute::Name(name) = attribute? { + parameter = Some(name); + } + } + depth += 1; + } + NamedElement { + ns: Namespace::CalDav, + element: Element::IsNotDefined, + } => { + stream.expect_element_end()?; + if let Some(filter) = Filter::from_parts( + components.iter().map(|(c, _)| *c).collect(), + property.clone(), + parameter.clone(), + FilterOp::Undefined, + ) { + cq.filters.push(filter); + } + } + NamedElement { + ns: Namespace::CalDav, + element: Element::TextMatch, + } => { + let mut tm = TextMatch::parse(raw)?; + tm.value = stream.collect_string_value()?.unwrap_or_default(); + if let Some(filter) = Filter::from_parts( + components.iter().map(|(c, _)| *c).collect(), + property.clone(), + parameter.clone(), + FilterOp::TextMatch(tm), + ) { + cq.filters.push(filter); + } + } + NamedElement { + ns: Namespace::CalDav, + element: Element::TimeRange, + } => { + let range = DateRange::from_raw(&raw)?; + stream.expect_element_end()?; + if let Some(filter) = Filter::from_parts( + components.iter().map(|(c, _)| *c).collect(), + property.clone(), + parameter.clone(), + FilterOp::TimeRange(range), + ) { + cq.filters.push(filter); + } + } + name => return Err(name.into_unexpected()), + }, + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + if matches!(components.last(), Some((_, d)) if *d == depth) { + if components.len() > 1 + && cq + .filters + .last() + .and_then(|c| c.components()) + .is_none_or(|c| c.len() < components.len()) + { + cq.filters.push(Filter::Component { + comp: components.iter().map(|(c, _)| *c).collect(), + op: FilterOp::Exists, + }); + } + components.pop(); + } + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + element => return Err(element.into_unexpected()), + } + } + + Ok(cq) + } +} + +impl DavParser for AddressbookQuery { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut aq = AddressbookQuery { + properties: PropFind::AllProp(vec![]), + filters: vec![], + limit: None, + }; + let mut depth = 1; + let mut property = None; + let mut parameter = None; + + loop { + match stream.token()? { + Token::ElementStart { name, raw } => match name { + NamedElement { + ns: Namespace::Dav, + element: Element::Propname, + } if depth == 1 => { + aq.properties = PropFind::PropName; + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::Allprop, + } if depth == 1 => { + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + } if depth == 1 => { + aq.properties = PropFind::Prop(stream.collect_properties()?); + } + NamedElement { + ns: Namespace::CardDav, + element: Element::Filter, + } if depth == 1 => { + aq.filters.push(Filter::parse(raw)?); + depth += 1; + } + NamedElement { + ns: Namespace::CardDav, + element: Element::Limit, + } if depth == 1 => { + stream.expect_named_element(NamedElement::carddav(Element::Nresults))?; + if let Some(Ok(limit)) = stream.parse_value::()? { + aq.limit = limit.into(); + } + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::CardDav, + element: Element::PropFilter, + } if depth == 2 => { + let mut filter = Filter::AnyOf; + for attribute in raw.attributes::() { + match attribute? { + Attribute::Name(name) => { + property = Some(name); + } + Attribute::TestAllOf(all_of) => { + filter = if all_of { Filter::AllOf } else { Filter::AnyOf }; + } + _ => {} + } + } + aq.filters.push(filter); + depth += 1; + } + NamedElement { + ns: Namespace::CardDav, + element: Element::ParamFilter, + } if depth == 3 => { + for attribute in raw.attributes::() { + if let Attribute::Name(name) = attribute? { + parameter = Some(name); + } + } + depth += 1; + } + NamedElement { + ns: Namespace::CardDav, + element: Element::IsNotDefined, + } => { + stream.expect_element_end()?; + if let Some(filter) = Filter::from_parts( + (), + property.clone(), + parameter.clone(), + FilterOp::Undefined, + ) { + aq.filters.push(filter); + } + } + NamedElement { + ns: Namespace::CardDav, + element: Element::TextMatch, + } => { + let mut tm = TextMatch::parse(raw)?; + tm.value = stream.collect_string_value()?.unwrap_or_default(); + if let Some(filter) = Filter::from_parts( + (), + property.clone(), + parameter.clone(), + FilterOp::TextMatch(tm), + ) { + aq.filters.push(filter); + } + } + name => return Err(name.into_unexpected()), + }, + Token::ElementEnd => { + depth -= 1; + if depth == 0 { + break; + } + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + element => return Err(element.into_unexpected()), + } + } + + Ok(aq) + } +} + +impl DavParser for FreeBusyQuery { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + match stream.token()? { + Token::ElementStart { + name: + NamedElement { + ns: Namespace::CalDav, + element: Element::TimeRange, + }, + raw, + } => DateRange::from_raw(&raw).map(|range| FreeBusyQuery { range }), + other => Err(other.into_unexpected()), + } + } +} + +impl DavParser for MultiGet { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut mg = MultiGet { + properties: PropFind::AllProp(vec![]), + hrefs: vec![], + }; + + loop { + match stream.token()? { + Token::ElementStart { name, .. } => match name { + NamedElement { + ns: Namespace::Dav, + element: Element::Propname, + } => { + mg.properties = PropFind::PropName; + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::Allprop, + } => { + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + } => { + mg.properties = PropFind::Prop(stream.collect_properties()?); + } + NamedElement { + ns: Namespace::Dav, + element: Element::Href, + } => { + if let Some(href) = stream.collect_string_value()? { + mg.hrefs.push(href); + } + } + name => return Err(name.into_unexpected()), + }, + Token::ElementEnd => { + break; + } + element => return Err(element.into_unexpected()), + } + } + + Ok(mg) + } +} + +impl DavParser for SyncCollection { + fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result { + let mut sc = SyncCollection { + properties: PropFind::AllProp(vec![]), + limit: None, + sync_token: None, + level_inf: false, + }; + + loop { + match stream.token()? { + Token::ElementStart { name, .. } => match name { + NamedElement { + ns: Namespace::Dav, + element: Element::Prop, + } => { + sc.properties = PropFind::Prop(stream.collect_properties()?); + } + NamedElement { + ns: Namespace::Dav, + element: Element::Limit, + } => { + stream.expect_named_element(NamedElement::dav(Element::Nresults))?; + if let Some(Ok(limit)) = stream.parse_value::()? { + sc.limit = limit.into(); + } + stream.expect_element_end()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::SyncToken, + } => { + sc.sync_token = stream.collect_string_value()?; + } + NamedElement { + ns: Namespace::Dav, + element: Element::SyncLevel, + } => { + if let Some(Ok(_)) = stream.parse_value::()? { + sc.level_inf = true; + } + } + name => return Err(name.into_unexpected()), + }, + Token::ElementEnd => { + break; + } + Token::UnknownElement(_) => { + stream.seek_element_end()?; + } + element => return Err(element.into_unexpected()), + } + } + + Ok(sc) + } +} + +impl TextMatch { + fn parse(raw: RawElement<'_>) -> crate::parser::Result { + let mut tm = TextMatch { + match_type: MatchType::Contains, + value: String::new(), + collation: Collation::AsciiCasemap, + negate: false, + }; + + for attribute in raw.attributes::() { + match attribute? { + Attribute::MatchType(match_type) => { + tm.match_type = match_type; + } + Attribute::NegateCondition(negate) => { + tm.negate = negate; + } + Attribute::Collation(collation) => { + tm.collation = collation; + } + _ => {} + } + } + + Ok(tm) + } +} + +impl Filter { + fn from_parts(comp: A, prop: Option, param: Option, op: FilterOp) -> Option { + match (prop, param) { + (Some(prop), Some(param)) => Some(Filter::Parameter { + comp, + prop, + param, + op, + }), + (Some(prop), None) => Some(Filter::Property { comp, prop, op }), + (None, None) => Some(Filter::Component { comp, op }), + _ => None, + } + } + + fn components(&self) -> Option<&A> { + match self { + Filter::Component { comp, .. } => Some(comp), + Filter::Property { comp, .. } => Some(comp), + Filter::Parameter { comp, .. } => Some(comp), + _ => None, + } + } + + fn parse(raw: RawElement<'_>) -> crate::parser::Result { + for attribute in raw.attributes::() { + if let Attribute::TestAllOf(all_of) = attribute? { + return Ok(if all_of { Filter::AllOf } else { Filter::AnyOf }); + } + } + + Ok(Filter::AnyOf) + } +} + +struct Infinite; + +impl XmlValueParser for Infinite { + fn parse_bytes(bytes: &[u8]) -> Option { + if bytes == b"infinite" { + Some(Infinite) + } else { + None + } + } + + fn parse_str(text: &str) -> Option { + if text == "infinite" { + Some(Infinite) + } else { + None + } + } +} diff --git a/crates/dav-proto/src/responses/acl.rs b/crates/dav-proto/src/responses/acl.rs new file mode 100644 index 00000000..132a0e26 --- /dev/null +++ b/crates/dav-proto/src/responses/acl.rs @@ -0,0 +1,301 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use crate::{ + responses::XmlEscape, + schema::{ + property::{DavProperty, Privilege}, + response::{ + Ace, AclRestrictions, GrantDeny, Href, List, Principal, PrincipalSearchProperty, + PrincipalSearchPropertySet, RequiredPrincipal, Resource, SupportedPrivilege, + }, + Namespace, + }, +}; + +impl Display for SupportedPrivilege { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.privilege)?; + if self.abstract_ { + write!(f, "")?; + } + write!(f, "")?; + self.description.write_escaped_to(f)?; + write!( + f, + "{}", + self.supported_privilege + ) + } +} + +impl SupportedPrivilege { + pub fn new(privilege: Privilege, description: impl Into) -> Self { + SupportedPrivilege { + privilege, + abstract_: false, + description: description.into(), + supported_privilege: List(vec![]), + } + } + + pub fn with_abstract(mut self) -> Self { + self.abstract_ = true; + self + } + + pub fn with_supported_privilege(mut self, supported_privilege: SupportedPrivilege) -> Self { + self.supported_privilege.0.push(supported_privilege); + self + } +} + +impl Display for Ace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + if self.invert { + write!(f, "")?; + } + self.principal.fmt(f)?; + if self.invert { + write!(f, "")?; + } + self.grant_deny.fmt(f)?; + if self.protected { + write!(f, "")?; + } + if let Some(inherited) = &self.inherited { + write!(f, "")?; + inherited.fmt(f)?; + write!(f, "")?; + } + write!(f, "") + } +} + +impl Display for Principal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + match self { + Principal::Href(href) => href.fmt(f), + Principal::All => "".fmt(f), + Principal::Authenticated => "".fmt(f), + Principal::Unauthenticated => "".fmt(f), + Principal::Property(property) => { + write!(f, "{}", property) + } + Principal::Self_ => "".fmt(f), + }?; + write!(f, "") + } +} + +impl Display for GrantDeny { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GrantDeny::Grant(privileges) => { + write!(f, "")?; + privileges.fmt(f)?; + write!(f, "") + } + GrantDeny::Deny(privileges) => { + write!(f, "")?; + privileges.fmt(f)?; + write!(f, "") + } + } + } +} + +impl Display for AclRestrictions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.grant_only { + write!(f, "")?; + } + if self.no_invert { + write!(f, "")?; + } + if self.deny_before_grant { + write!(f, "")?; + } + if let Some(required_principal) = &self.required_principal { + required_principal.fmt(f)?; + } + Ok(()) + } +} + +impl Display for RequiredPrincipal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + match self { + RequiredPrincipal::All => "".fmt(f)?, + RequiredPrincipal::Authenticated => "".fmt(f)?, + RequiredPrincipal::Unauthenticated => "".fmt(f)?, + RequiredPrincipal::Self_ => "".fmt(f)?, + RequiredPrincipal::Href(hrefs) => hrefs.fmt(f)?, + RequiredPrincipal::Property(properties) => { + for property in properties { + write!(f, "{}", property)?; + } + } + } + write!(f, "") + } +} + +impl Display for Privilege { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Privilege::Read => "".fmt(f), + Privilege::Write => "".fmt(f), + Privilege::WriteProperties => "".fmt(f), + Privilege::WriteContent => "".fmt(f), + Privilege::Unlock => "".fmt(f), + Privilege::ReadAcl => "".fmt(f), + Privilege::ReadCurrentUserPrivilegeSet => { + "".fmt(f) + } + Privilege::WriteAcl => "".fmt(f), + Privilege::Bind => "".fmt(f), + Privilege::Unbind => "".fmt(f), + Privilege::All => "".fmt(f), + Privilege::ReadFreeBusy => "".fmt(f), + } + } +} + +impl Display for Resource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}{}", + self.href, self.privilege + ) + } +} + +impl Display for PrincipalSearchPropertySet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.namespace, self.properties + ) + } +} + +impl Display for PrincipalSearchProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.name + )?; + write!( + f, + "{}", + self.description + ) + } +} + +impl Resource { + pub fn new(href: impl Into, privilege: Privilege) -> Self { + Resource { + href: Href(href.into()), + privilege, + } + } +} + +impl PrincipalSearchPropertySet { + pub fn new(properties: Vec) -> Self { + PrincipalSearchPropertySet { + namespace: Namespace::Dav, + properties: List(properties), + } + } + + pub fn with_namespace(mut self, namespace: Namespace) -> Self { + self.namespace = namespace; + self + } +} + +impl PrincipalSearchProperty { + pub fn new(name: impl Into, description: impl Into) -> Self { + PrincipalSearchProperty { + name: name.into(), + description: description.into(), + } + } +} + +impl Ace { + pub fn new(principal: Principal, grant_deny: GrantDeny) -> Self { + Ace { + principal, + invert: false, + grant_deny, + protected: false, + inherited: None, + } + } + + pub fn with_invert(mut self) -> Self { + self.invert = true; + self + } + + pub fn with_protected(mut self) -> Self { + self.protected = true; + self + } + + pub fn with_inherited(mut self, inherited: impl Into) -> Self { + self.inherited = Some(Href(inherited.into())); + self + } +} + +impl GrantDeny { + pub fn grant(privileges: Vec) -> Self { + GrantDeny::Grant(List(privileges)) + } + + pub fn deny(privileges: Vec) -> Self { + GrantDeny::Deny(List(privileges)) + } +} + +impl AclRestrictions { + pub fn new() -> Self { + Self::default() + } + + pub fn with_grant_only(mut self) -> Self { + self.grant_only = true; + self + } + + pub fn with_no_invert(mut self) -> Self { + self.no_invert = true; + self + } + + pub fn with_deny_before_grant(mut self) -> Self { + self.deny_before_grant = true; + self + } + + pub fn with_required_principal(mut self, required_principal: RequiredPrincipal) -> Self { + self.required_principal = Some(required_principal); + self + } +} diff --git a/crates/dav-proto/src/responses/error.rs b/crates/dav-proto/src/responses/error.rs new file mode 100644 index 00000000..aa95bfea --- /dev/null +++ b/crates/dav-proto/src/responses/error.rs @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use crate::schema::{ + response::{BaseCondition, CalCondition, CardCondition, Condition, ErrorResponse}, + Namespace, +}; + +impl Display for ErrorResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "", self.namespace)?; + + match &self.error { + Condition::Base(e) => e.fmt(f)?, + Condition::Cal(e) => e.fmt(f)?, + Condition::Card(e) => e.fmt(f)?, + } + + write!(f, "") + } +} + +impl Display for Condition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + + match self { + Condition::Base(e) => e.fmt(f)?, + Condition::Cal(e) => e.fmt(f)?, + Condition::Card(e) => e.fmt(f)?, + } + + write!(f, "") + } +} + +impl Display for BaseCondition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BaseCondition::NoConflictingLock(items) => { + write!(f, "{items}") + } + BaseCondition::LockTokenSubmitted(items) => write!( + f, + "{items}" + ), + BaseCondition::LockTokenMatchesRequestUri => { + write!(f, "") + } + BaseCondition::CannotModifyProtectedProperty => { + write!(f, "") + } + BaseCondition::NoExternalEntities => write!(f, ""), + BaseCondition::PreservedLiveProperties => write!(f, ""), + BaseCondition::PropFindFiniteDepth => write!(f, ""), + BaseCondition::ResourceMustBeNull => write!(f, ""), + BaseCondition::NeedPrivileges(resources) => { + write!(f, "{resources}") + } + BaseCondition::NumberOfMatchesWithinLimit => { + write!(f, "") + } + BaseCondition::QuotaNotExceeded => write!(f, ""), + BaseCondition::ValidResourceType => write!(f, ""), + BaseCondition::ValidSyncToken => write!(f, ""), + BaseCondition::NoAceConflict => write!(f, ""), + BaseCondition::NoProtectedAceConflict => write!(f, ""), + BaseCondition::NoInheritedAceConflict => write!(f, ""), + BaseCondition::LimitedNumberOfAces => write!(f, ""), + BaseCondition::DenyBeforeGrant => write!(f, ""), + BaseCondition::GrantOnly => write!(f, ""), + BaseCondition::NoInvert => write!(f, ""), + BaseCondition::NoAbstract => write!(f, ""), + BaseCondition::NotSupportedPrivilege => write!(f, ""), + BaseCondition::MissingRequiredPrincipal => write!(f, ""), + BaseCondition::RecognizedPrincipal => write!(f, ""), + BaseCondition::AllowedPrincipal => write!(f, ""), + } + } +} + +impl Display for CalCondition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CalCondition::CalendarCollectionLocationOk => { + write!(f, "") + } + CalCondition::ValidCalendarData => write!(f, ""), + CalCondition::ValidFilter => write!(f, ""), + CalCondition::ValidCalendarObjectResource => { + write!(f, "") + } + CalCondition::NoUidConflict(uid) => { + write!(f, "{uid}") + } + CalCondition::InitializeCalendarCollection => { + write!(f, "") + } + CalCondition::SupportedCalendarData => write!(f, ""), + CalCondition::SupportedFilter(_) => write!(f, ""), + CalCondition::SupportedCollation(c) => { + write!(f, "{c}") + } + CalCondition::MinDateTime => write!(f, ""), + CalCondition::MaxDateTime => write!(f, ""), + CalCondition::MaxResourceSize(l) => { + write!(f, "{l}") + } + CalCondition::MaxInstances => write!(f, ""), + CalCondition::MaxAttendeesPerInstance => write!(f, ""), + } + } +} + +impl Display for CardCondition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CardCondition::SupportedAddressData => write!(f, ""), + CardCondition::SupportedAddressDataConversion => { + write!(f, "") + } + CardCondition::SupportedFilter(_) => write!(f, ""), + CardCondition::SupportedCollation(c) => { + write!(f, "{c}") + } + CardCondition::ValidAddressData => write!(f, ""), + CardCondition::NoUidConflict(uid) => { + write!(f, "{uid}") + } + CardCondition::MaxResourceSize(l) => { + write!(f, "{l}") + } + CardCondition::AddressBoolCollectionLocationOk => { + write!(f, "") + } + } + } +} + +impl From for Condition { + fn from(error: CalCondition) -> Self { + Condition::Cal(error) + } +} + +impl From for Condition { + fn from(error: CardCondition) -> Self { + Condition::Card(error) + } +} + +impl From for Condition { + fn from(error: BaseCondition) -> Self { + Condition::Base(error) + } +} + +impl ErrorResponse { + pub fn new(error: impl Into) -> Self { + ErrorResponse { + namespace: Namespace::Dav, + error: error.into(), + } + } + + pub fn with_namespace(mut self, namespace: impl Into) -> Self { + self.namespace = namespace.into(); + self + } +} diff --git a/crates/dav-proto/src/responses/lock.rs b/crates/dav-proto/src/responses/lock.rs new file mode 100644 index 00000000..dddcd8a3 --- /dev/null +++ b/crates/dav-proto/src/responses/lock.rs @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use crate::{ + schema::{ + property::{ActiveLock, LockDiscovery, LockEntry, LockScope, LockType, SupportedLock}, + request::{DeadProperty, LockInfo}, + response::{Href, List}, + }, + Depth, Timeout, +}; + +impl Display for SupportedLock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for LockDiscovery { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Display for ActiveLock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}{}{}", + self.lock_scope, self.lock_type, self.depth + )?; + + if let Some(owner) = &self.owner { + write!(f, "{}", owner)?; + } + + write!(f, "{}", self.timeout)?; + + if let Some(lock_token) = &self.lock_token { + write!(f, "{}", lock_token)?; + } + + write!( + f, + "{}", + self.lock_root + ) + } +} + +impl Display for Depth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Depth::Zero => write!(f, "0"), + Depth::One => write!(f, "1"), + Depth::Infinity => write!(f, "infinity"), + Depth::None => write!(f, ""), + } + } +} + +impl Display for Timeout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Timeout::Infinite => write!(f, "Infinite"), + Timeout::Second(s) => write!(f, "Second-{}", s), + Timeout::None => Ok(()), + } + } +} + +impl Display for LockInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.lock_scope, self.lock_type)?; + + if let Some(owner) = &self.owner { + write!(f, "{}", owner)?; + } + + write!(f, "",) + } +} + +impl Display for LockEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}{}", + self.lock_scope, self.lock_type + ) + } +} + +impl Display for LockScope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LockScope::Exclusive => write!(f, ""), + LockScope::Shared => write!(f, ""), + } + } +} + +impl Display for LockType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LockType::Write => write!(f, ""), + LockType::Other => write!(f, ""), + } + } +} + +impl ActiveLock { + pub fn new(href: impl Into, lock_scope: LockScope) -> Self { + Self { + lock_scope, + lock_type: LockType::Write, + depth: Depth::Infinity, + owner: None, + timeout: Timeout::Infinite, + lock_token: None, + lock_root: Href(href.into()), + } + } + + pub fn with_depth(mut self, depth: Depth) -> Self { + self.depth = depth; + self + } + + pub fn with_timeout(mut self, timeout: u64) -> Self { + self.timeout = Timeout::Second(timeout); + self + } + + pub fn with_owner_opt(mut self, owner: Option) -> Self { + self.owner = owner; + self + } + + pub fn with_owner(mut self, owner: DeadProperty) -> Self { + self.owner = Some(owner); + self + } + + pub fn with_lock_token(mut self, token: impl Into) -> Self { + self.lock_token = Some(Href(token.into())); + self + } +} + +impl Default for SupportedLock { + fn default() -> Self { + Self(List(vec![ + LockEntry { + lock_scope: LockScope::Exclusive, + lock_type: LockType::Write, + }, + LockEntry { + lock_scope: LockScope::Shared, + lock_type: LockType::Write, + }, + ])) + } +} diff --git a/crates/dav-proto/src/responses/mkcol.rs b/crates/dav-proto/src/responses/mkcol.rs new file mode 100644 index 00000000..e2c670d4 --- /dev/null +++ b/crates/dav-proto/src/responses/mkcol.rs @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use crate::schema::{ + response::{List, MkColResponse, PropStat}, + Namespace, +}; + +impl Display for MkColResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.namespace, self.propstat + ) + } +} + +impl MkColResponse { + pub fn new(propstat: Vec) -> Self { + Self { + namespace: Namespace::Dav, + propstat: List(propstat), + } + } + + pub fn with_namespace(mut self, namespace: Namespace) -> Self { + self.namespace = namespace; + self + } +} diff --git a/crates/dav-proto/src/responses/mod.rs b/crates/dav-proto/src/responses/mod.rs new file mode 100644 index 00000000..abbd23dd --- /dev/null +++ b/crates/dav-proto/src/responses/mod.rs @@ -0,0 +1,636 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +pub mod acl; +pub mod error; +pub mod lock; +pub mod mkcol; +pub mod multistatus; +pub mod property; +pub mod propstat; + +use std::fmt::{Display, Write}; + +use crate::schema::{ + property::{Comp, ResourceType, SupportedCollation}, + request::{DeadProperty, DeadPropertyTag}, + response::{Href, List, Location, ResponseDescription, Status, SyncToken}, + Namespace, +}; + +trait XmlEscape { + fn write_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result; +} + +impl> XmlEscape for T { + fn write_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result { + let str = self.as_ref(); + + for c in str.chars() { + match c { + '<' => out.write_str("<")?, + '>' => out.write_str(">")?, + '&' => out.write_str("&")?, + '"' => out.write_str(""")?, + '\'' => out.write_str("'")?, + _ => out.write_char(c)?, + } + } + + Ok(()) + } +} + +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 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.write_to(f) + } +} + +impl Display for Href { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + self.0.write_escaped_to(f)?; + write!(f, "") + } +} + +impl Display for List { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for item in &self.0 { + item.fmt(f)?; + } + + Ok(()) + } +} + +impl Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + write!(f, "HTTP/1.1 {}", self.0)?; + write!(f, "") + } +} + +impl Display for ResponseDescription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + self.0.write_escaped_to(f)?; + write!(f, "") + } +} + +impl Display for Location { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + self.0.fmt(f)?; + write!(f, "") + } +} + +impl Display for SyncToken { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + self.0.write_escaped_to(f)?; + write!(f, "") + } +} + +impl Display for Comp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "", self.0.as_str()) + } +} + +impl Display for ResourceType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResourceType::Collection => write!(f, ""), + ResourceType::Principal => write!(f, ""), + ResourceType::AddressBook => write!(f, ""), + ResourceType::Calendar => write!(f, ""), + } + } +} + +impl Display for SupportedCollation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.0.as_str() + ) + } +} + +impl Display for DeadProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut last_tag = ""; + + for item in &self.0 { + match item { + DeadPropertyTag::ElementStart(tag) => { + let name = &tag.name; + if let Some(attrs) = &tag.attrs { + write!(f, "<{name} {attrs}>")?; + } else { + write!(f, "<{name}>")?; + } + last_tag = name; + } + DeadPropertyTag::ElementEnd => { + write!(f, "", last_tag)?; + } + DeadPropertyTag::Text(text) => { + text.write_escaped_to(f)?; + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::fmt::Display; + + use calcard::{icalendar::ICalendar, vcard::VCard}; + use hyper::StatusCode; + use mail_parser::DateTime; + + use crate::{ + parser::{tokenizer::Tokenizer, Token}, + schema::{ + property::{ + ActiveLock, CalDavProperty, CardDavProperty, DavValue, LockScope, Privilege, + ResourceType, Rfc1123DateTime, SupportedLock, WebDavProperty, + }, + request::{DavPropertyValue, DeadElementTag, DeadProperty, DeadPropertyTag}, + response::{ + Ace, AclRestrictions, BaseCondition, ErrorResponse, GrantDeny, Href, List, + MkColResponse, MultiStatus, Principal, PrincipalSearchProperty, + PrincipalSearchPropertySet, PropResponse, PropStat, RequiredPrincipal, Resource, + Response, SupportedPrivilege, + }, + Namespace, + }, + Depth, + }; + + impl List { + pub fn new(vec: impl IntoIterator) -> Self { + List(vec.into_iter().collect()) + } + } + + #[test] + fn parse_responses() { + for (num, test) in [ + // 001.xml + ErrorResponse::new(BaseCondition::LockTokenSubmitted(List::new([Href( + "/locked/".to_string(), + )]))) + .to_string(), + // 002.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/file", + vec![ + PropStat::new(DavPropertyValue::new( + WebDavProperty::DisplayName, + "Box type A", + )), + PropStat::new(DavPropertyValue::new( + WebDavProperty::DisplayName, + "Box type B", + )) + .with_status(StatusCode::FORBIDDEN) + .with_response_description( + "The user does not have access to the DingALing property.", + ), + ], + )]) + .with_response_description("There has been an access violation error.") + .to_string(), + // 003.xml + MultiStatus::new(vec![ + Response::new_propstat( + "/container/", + vec![PropStat::new_list(vec![ + DavPropertyValue::new( + WebDavProperty::CreationDate, + DateTime::parse_rfc3339("1997-12-01T17:42:21-08:00Z").unwrap(), + ), + DavPropertyValue::new(WebDavProperty::DisplayName, "Example collection"), + DavPropertyValue::new( + WebDavProperty::ResourceType, + vec![ResourceType::Collection], + ), + DavPropertyValue::new( + WebDavProperty::SupportedLock, + SupportedLock::default(), + ), + ])], + ), + Response::new_propstat( + "/container/front.html", + vec![PropStat::new_list(vec![ + DavPropertyValue::new( + WebDavProperty::CreationDate, + DateTime::parse_rfc3339("1997-12-01T18:27:21-08:00").unwrap(), + ), + DavPropertyValue::new(WebDavProperty::DisplayName, "Example HTML resource"), + DavPropertyValue::new(WebDavProperty::GetContentLength, 4525u64), + DavPropertyValue::new(WebDavProperty::GetContentType, "text/html"), + DavPropertyValue::new(WebDavProperty::GetETag, "\"zzyzx\""), + DavPropertyValue::new( + WebDavProperty::GetLastModified, + DavValue::Rfc1123Date(Rfc1123DateTime::new( + DateTime::parse_rfc822("Mon, 12 Jan 1998 09:25:56 GMT") + .unwrap() + .to_timestamp(), + )), + ), + DavPropertyValue::new(WebDavProperty::ResourceType, DavValue::Null), + DavPropertyValue::new( + WebDavProperty::SupportedLock, + SupportedLock::default(), + ), + ])], + ), + ]) + .to_string(), + // 004.xml + MultiStatus::new(vec![Response::new_status( + ["http://www.example.com/container/resource3"], + StatusCode::LOCKED, + ) + .with_error(BaseCondition::LockTokenSubmitted(List(vec![])))]) + .to_string(), + // 005.xml + PropResponse::new(vec![DavPropertyValue::new( + WebDavProperty::LockDiscovery, + vec![ActiveLock::new( + "http://example.com/workspace/webdav/proposal.doc", + LockScope::Exclusive, + ) + .with_owner(DeadProperty(vec![ + DeadPropertyTag::ElementStart(DeadElementTag { + name: "D:href".to_string(), + attrs: None, + }), + DeadPropertyTag::Text("http://example.org/~ejw/contact.html".to_string()), + DeadPropertyTag::ElementEnd, + ])) + .with_timeout(604800) + .with_lock_token("urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4")], + )]) + .to_string(), + // 006.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/container/", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::LockDiscovery, + vec![ + ActiveLock::new("http://www.example.com/container/", LockScope::Shared) + .with_owner(DeadProperty(vec![DeadPropertyTag::Text( + "Jane Smith".to_string(), + )])) + .with_depth(Depth::Zero) + .with_lock_token("urn:uuid:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76"), + ], + )])], + )]) + .to_string(), + // 007.xml + ErrorResponse::new(BaseCondition::LockTokenSubmitted(List(vec![Href( + "/workspace/webdav/".to_string(), + )]))) + .to_string(), + // 008.xml + MultiStatus::new(vec![ + Response::new_propstat( + "http://cal.example.com/bernard/work/abcd2.ics", + vec![PropStat::new_list(vec![ + DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd2\""), + DavPropertyValue::new( + CalDavProperty::CalendarData(Default::default()), + ICalendar::parse( + r#"BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060106T140000 +DURATION:PT1H +RECURRENCE-ID;TZID=US/Eastern:20060106T120000 +SUMMARY:Event #2 bis bis +UID:00959BC664CA650E933C892C@example.com +END:VEVENT +END:VCALENDAR +"#, + ) + .unwrap(), + ), + ])], + ), + Response::new_propstat( + "http://cal.example.com/bernard/work/abcd3.ics", + vec![PropStat::new_list(vec![ + DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd3\""), + DavPropertyValue::new( + CalDavProperty::CalendarData(Default::default()), + ICalendar::parse( + r#"BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//CalDAV Client//EN +BEGIN:VEVENT +DTSTART;TZID=US/Eastern:20060104T100000 +DURATION:PT1H +SUMMARY:Event #3 +UID:DC6C50A017428C5216A2F1CD@example.com +END:VEVENT +END:VCALENDAR +"#, + ) + .unwrap(), + ), + ])], + ), + ]) + .with_namespace(Namespace::CalDav) + .to_string(), + // 009.xml + MkColResponse::new(vec![PropStat::new_list(vec![ + DavPropertyValue::new(WebDavProperty::ResourceType, DavValue::Null), + DavPropertyValue::new(WebDavProperty::DisplayName, DavValue::Null), + DavPropertyValue::new(CardDavProperty::AddressbookDescription, DavValue::Null), + ])]) + .with_namespace(Namespace::CardDav) + .to_string(), + // 010.xml + MultiStatus::new(vec![Response::new_propstat( + "/home/bernard/addressbook/v102.vcf", + vec![PropStat::new_list(vec![ + DavPropertyValue::new(WebDavProperty::GetETag, "\"23ba4d-ff11fb\""), + DavPropertyValue::new( + CardDavProperty::AddressData(Default::default()), + VCard::parse( + r#"BEGIN:VCARD +VERSION:3.0 +NICKNAME:me +UID:34222-232@example.com +FN:Cyrus Daboo +EMAIL:daboo@example.com +END:VCARD +"#, + ) + .unwrap(), + ), + ])], + )]) + .with_namespace(Namespace::CardDav) + .to_string(), + // 011.xml + MultiStatus::new(vec![ + Response::new_status( + ["/home/bernard/addressbook/"], + StatusCode::INSUFFICIENT_STORAGE, + ) + .with_error(BaseCondition::NumberOfMatchesWithinLimit) + .with_response_description("Only two matching records were returned"), + Response::new_propstat( + "/home/bernard/addressbook/v102.vcf", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::GetETag, + "\"23ba4d-ff11fb\"", + )])], + ), + Response::new_propstat( + "/home/bernard/addressbook/v104.vcf", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::GetETag, + "\"23ba4d-ff11fc\"", + )])], + ), + ]) + .with_namespace(Namespace::CardDav) + .to_string(), + // 012.xml + ErrorResponse::new(BaseCondition::NeedPrivileges(List(vec![ + Resource::new("/a", Privilege::Unbind), + Resource::new("/c", Privilege::Bind), + ]))) + .to_string(), + // 013.xml + PrincipalSearchPropertySet::new(vec![ + PrincipalSearchProperty::new(WebDavProperty::DisplayName, "Full name"), + PrincipalSearchProperty::new(WebDavProperty::DisplayName, "Job title"), + ]) + .to_string(), + // 014.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/papers/", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::SupportedPrivilegeSet, + vec![SupportedPrivilege::new(Privilege::All, "Any operation") + .with_abstract() + .with_supported_privilege( + SupportedPrivilege::new(Privilege::Read, "Read any object") + .with_supported_privilege( + SupportedPrivilege::new(Privilege::ReadAcl, "Read ACL") + .with_abstract(), + ) + .with_supported_privilege( + SupportedPrivilege::new( + Privilege::ReadCurrentUserPrivilegeSet, + "Read current user privilege set property", + ) + .with_abstract(), + ), + ) + .with_supported_privilege( + SupportedPrivilege::new(Privilege::Write, "Write any object") + .with_supported_privilege( + SupportedPrivilege::new(Privilege::WriteAcl, "Write ACL") + .with_abstract(), + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteProperties, + "Write properties", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteContent, + "Write resource content", + )), + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Unlock, + "Unlock resource", + ))], + )])], + )]) + .to_string(), + // 015.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/papers/", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::CurrentUserPrivilegeSet, + vec![Privilege::Read], + )])], + )]) + .to_string(), + // 016.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/papers/", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::Acl, + vec![ + Ace::new( + Principal::Href(Href( + "http://www.example.com/acl/groups/maintainers".to_string(), + )), + GrantDeny::grant(vec![Privilege::Write]), + ), + Ace::new(Principal::All, GrantDeny::grant(vec![Privilege::Read])), + ], + )])], + )]) + .to_string(), + // 017.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/papers/", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::AclRestrictions, + AclRestrictions::new() + .with_grant_only() + .with_required_principal(RequiredPrincipal::All), + )])], + )]) + .to_string(), + // 018.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/papers/", + vec![PropStat::new_list(vec![DavPropertyValue::new( + WebDavProperty::PrincipalCollectionSet, + vec![ + Href("http://www.example.com/acl/users/".to_string()), + Href("http://www.example.com/acl/groups/".to_string()), + ], + )])], + )]) + .to_string(), + // 019.xml + MultiStatus::new(vec![Response::new_propstat( + "http://www.example.com/top/container/", + vec![PropStat::new_list(vec![ + DavPropertyValue::new( + WebDavProperty::Owner, + vec![Href("http://www.example.com/users/gclemm".to_string())], + ), + DavPropertyValue::new( + WebDavProperty::SupportedPrivilegeSet, + vec![SupportedPrivilege::new(Privilege::All, "Any operation") + .with_abstract() + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Read, + "Read any object", + )) + .with_supported_privilege( + SupportedPrivilege::new(Privilege::Write, "Write any object") + .with_abstract(), + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::ReadAcl, + "Read the ACL", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteAcl, + "Write the ACL", + ))], + ), + DavPropertyValue::new( + WebDavProperty::CurrentUserPrivilegeSet, + vec![Privilege::Read, Privilege::ReadAcl], + ), + DavPropertyValue::new( + WebDavProperty::Acl, + vec![ + Ace::new( + Principal::Href(Href( + "http://www.example.com/users/esedlar".to_string(), + )), + GrantDeny::grant(vec![ + Privilege::Read, + Privilege::Write, + Privilege::ReadAcl, + ]), + ), + Ace::new( + Principal::Href(Href( + "http://www.example.com/groups/mrktng".to_string(), + )), + GrantDeny::deny(vec![Privilege::Read]), + ), + Ace::new( + Principal::Property(List(vec![DavPropertyValue::new( + WebDavProperty::Owner, + DavValue::Null, + )])), + GrantDeny::grant(vec![Privilege::ReadAcl, Privilege::WriteAcl]), + ), + Ace::new(Principal::All, GrantDeny::grant(vec![Privilege::Read])) + .with_inherited("http://www.example.com/top"), + ], + ), + ])], + )]) + .to_string(), + ] + .into_iter() + .enumerate() + { + let xml = + std::fs::read_to_string(format!("resources/responses/{:03}.xml", num + 1)).unwrap(); + let mut output_token = Tokenizer::new(test.as_bytes()); + let mut expected_token = Tokenizer::new(xml.as_bytes()); + + loop { + let mut output = output_token.token().unwrap(); + let mut expected = expected_token.token().unwrap(); + + for token in [&mut output, &mut expected] { + if let Token::Bytes(text) = token { + // Remove '\r' + *text = text + .iter() + .copied() + .filter(|&c| c != b'\r') + .collect::>() + .into(); + } + } + + if output != expected { + eprintln!("{test}"); + } + assert_eq!(output, expected, "failed for {:03}.xml", num + 1); + if output == Token::Eof { + break; + } + } + } + } +} diff --git a/crates/dav-proto/src/responses/multistatus.rs b/crates/dav-proto/src/responses/multistatus.rs new file mode 100644 index 00000000..25e17d23 --- /dev/null +++ b/crates/dav-proto/src/responses/multistatus.rs @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use hyper::StatusCode; + +use crate::schema::{ + response::{ + Condition, Href, List, Location, MultiStatus, PropStat, Response, ResponseDescription, + ResponseType, Status, SyncToken, + }, + Namespace, +}; + +impl Display for MultiStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.namespace, self.response)?; + if let Some(response_description) = &self.response_description { + write!(f, "{response_description}")?; + } + + if let Some(sync_token) = &self.sync_token { + write!(f, "{sync_token}")?; + } + + write!(f, "") + } +} + +impl Display for Response { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + self.href.fmt(f)?; + self.typ.fmt(f)?; + if let Some(error) = &self.error { + error.fmt(f)?; + } + if let Some(response_description) = &self.response_description { + response_description.fmt(f)?; + } + if let Some(location) = &self.location { + location.fmt(f)?; + } + write!(f, "") + } +} + +impl Display for ResponseType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResponseType::PropStat(list) => list.fmt(f), + ResponseType::Status { href, status } => { + href.fmt(f)?; + status.fmt(f) + } + } + } +} + +impl MultiStatus { + pub fn new(response: Vec) -> Self { + MultiStatus { + namespace: Namespace::Dav, + response: List(response), + response_description: None, + sync_token: None, + } + } + + pub fn with_response(mut self, response: Response) -> Self { + self.response.0.push(response); + self + } + + pub fn add_response(&mut self, response: Response) { + self.response.0.push(response); + } + + pub fn with_response_description(mut self, response_description: impl Into) -> Self { + self.response_description = Some(ResponseDescription(response_description.into())); + self + } + + pub fn with_namespace(mut self, namespace: Namespace) -> Self { + self.namespace = namespace; + self + } + + pub fn with_sync_token(mut self, sync_token: impl Into) -> Self { + self.sync_token = Some(SyncToken(sync_token.into())); + self + } +} + +impl Response { + pub fn new_propstat(href: impl Into, propstat: Vec) -> Self { + Response { + href: href.into(), + typ: ResponseType::PropStat(List(propstat)), + error: None, + response_description: None, + location: None, + } + } + + pub fn new_status(href: T, status: StatusCode) -> Self + where + T: IntoIterator, + H: Into, + { + let mut href = href.into_iter().map(|h| Href(h.into())); + Response { + href: href.next().unwrap(), + typ: ResponseType::Status { + href: List(href.collect()), + status: Status(status), + }, + error: None, + response_description: None, + location: None, + } + } + + pub fn with_error(mut self, error: impl Into) -> Self { + self.error = Some(error.into()); + self + } + + pub fn with_response_description(mut self, response_description: impl Into) -> Self { + self.response_description = Some(ResponseDescription(response_description.into())); + self + } + + pub fn with_location(mut self, location: impl Into) -> Self { + self.location = Some(Location(Href(location.into()))); + self + } +} diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs new file mode 100644 index 00000000..c097d3ba --- /dev/null +++ b/crates/dav-proto/src/responses/property.rs @@ -0,0 +1,354 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use calcard::{icalendar::ICalendar, vcard::VCard}; +use mail_parser::{ + parsers::fields::date::{DOW, MONTH}, + DateTime, +}; + +use crate::schema::{ + property::{ + ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue, LockDiscovery, + LockEntry, Privilege, ReportSet, ResourceType, Rfc1123DateTime, SupportedCollation, + SupportedLock, WebDavProperty, + }, + request::{DavPropertyValue, DeadProperty}, + response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege}, + Namespace, +}; + +use super::XmlEscape; + +impl Display for PropResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.namespace, self.properties) + } +} + +impl Display for DavPropertyValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (name, attrs) = self.property.tag_name(); + if let Some(attrs) = attrs { + write!(f, "<{} {}>{}", name, attrs, self.value, name) + } else { + write!(f, "<{}>{}", name, self.value, name) + } + } +} + +impl Display for Rfc1123DateTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let dt = DateTime::from_timestamp(self.0); + write!( + f, + "{}, {} {} {:04} {:02}:{:02}:{:02} GMT", + DOW[dt.day_of_week() as usize], + dt.day, + MONTH + .get(dt.month.saturating_sub(1) as usize) + .copied() + .unwrap_or_default(), + dt.year, + dt.hour, + dt.minute, + dt.second, + ) + } +} + +impl Display for DavValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DavValue::Timestamp(v) => { + let dt = DateTime::from_timestamp(*v); + write!( + f, + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, + ) + } + DavValue::Rfc1123Date(v) => v.fmt(f), + DavValue::Uint64(v) => v.fmt(f), + DavValue::String(v) => v.write_escaped_to(f), + DavValue::ResourceTypes(v) => v.fmt(f), + DavValue::ActiveLocks(v) => v.fmt(f), + DavValue::LockEntries(v) => v.fmt(f), + DavValue::ReportSets(v) => v.fmt(f), + DavValue::VCard(v) => { + write!(f, "") + } + DavValue::ICalendar(v) => { + write!(f, "") + } + DavValue::Components(v) => v.fmt(f), + DavValue::Collations(v) => v.fmt(f), + DavValue::Href(v) => v.fmt(f), + DavValue::PrivilegeSet(v) => v.fmt(f), + DavValue::Privileges(v) => v.fmt(f), + DavValue::Acl(v) => v.fmt(f), + DavValue::AclRestrictions(v) => v.fmt(f), + DavValue::DeadProperty(v) => v.fmt(f), + DavValue::Null => Ok(()), + } + } +} + +impl DavProperty { + fn tag_name(&self) -> (&str, Option<&str>) { + ( + match self { + DavProperty::WebDav(prop) => match prop { + WebDavProperty::CreationDate => "D:creationdate", + WebDavProperty::DisplayName => "D:displayname", + WebDavProperty::GetContentLanguage => "D:getcontentlanguage", + WebDavProperty::GetContentLength => "D:getcontentlength", + WebDavProperty::GetContentType => "D:getcontenttype", + WebDavProperty::GetETag => "D:getetag", + WebDavProperty::GetLastModified => "D:getlastmodified", + WebDavProperty::ResourceType => "D:resourcetype", + WebDavProperty::LockDiscovery => "D:lockdiscovery", + WebDavProperty::SupportedLock => "D:supportedlock", + WebDavProperty::CurrentUserPrincipal => "D:current-user-principal", + WebDavProperty::QuotaAvailableBytes => "D:quota-available-bytes", + WebDavProperty::QuotaUsedBytes => "D:quota-used-bytes", + WebDavProperty::SupportedReportSet => "D:supported-report-set", + WebDavProperty::SyncToken => "D:sync-token", + WebDavProperty::AlternateURISet => "D:alternate-URI-set", + WebDavProperty::PrincipalURL => "D:principal-URL", + WebDavProperty::GroupMemberSet => "D:group-member-set", + WebDavProperty::GroupMembership => "D:group-membership", + WebDavProperty::Owner => "D:owner", + WebDavProperty::Group => "D:group", + WebDavProperty::SupportedPrivilegeSet => "D:supported-privilege-set", + WebDavProperty::CurrentUserPrivilegeSet => "D:current-user-privilege-set", + WebDavProperty::Acl => "D:acl", + WebDavProperty::AclRestrictions => "D:acl-restrictions", + WebDavProperty::InheritedAclSet => "D:inherited-acl-set", + WebDavProperty::PrincipalCollectionSet => "D:principal-collection-set", + }, + 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", + }, + DavProperty::CalDav(prop) => match prop { + CalDavProperty::CalendarDescription => "C:calendar-description", + CalDavProperty::CalendarTimezone => "C:calendar-timezone", + CalDavProperty::SupportedCalendarComponentSet => { + "C: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", + }, + DavProperty::DeadProperty(dead) => { + return (dead.name.as_str(), dead.attrs.as_deref()) + } + }, + None, + ) + } +} + +impl Display for ReportSet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReportSet::SyncCollection => write!(f, ""), + } + } +} + +impl Display for DavProperty { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (name, attrs) = self.tag_name(); + if let Some(attrs) = attrs { + write!(f, "<{name} {attrs}/>") + } else { + write!(f, "<{name}/>") + } + } +} + +impl PropResponse { + pub fn new(properties: Vec) -> Self { + PropResponse { + namespace: Namespace::Dav, + properties: List(properties), + } + } + + pub fn with_namespace(mut self, namespace: Namespace) -> Self { + self.namespace = namespace; + self + } +} + +impl From for DavProperty { + fn from(prop: WebDavProperty) -> Self { + DavProperty::WebDav(prop) + } +} + +impl From for DavProperty { + fn from(prop: CardDavProperty) -> Self { + DavProperty::CardDav(prop) + } +} + +impl From for DavProperty { + fn from(prop: CalDavProperty) -> Self { + DavProperty::CalDav(prop) + } +} + +impl From for DavValue { + fn from(v: String) -> Self { + DavValue::String(v) + } +} + +impl From<&str> for DavValue { + fn from(v: &str) -> Self { + DavValue::String(v.to_string()) + } +} + +impl From for DavValue { + fn from(v: u64) -> Self { + DavValue::Uint64(v) + } +} + +impl From for DavValue { + fn from(v: DateTime) -> Self { + DavValue::Timestamp(v.to_timestamp()) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::ResourceTypes(List(v)) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::ReportSets(List(v)) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::Components(List(v)) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::Collations(List(v)) + } +} + +impl From for DavValue { + fn from(v: ICalendar) -> Self { + DavValue::ICalendar(v) + } +} + +impl From for DavValue { + fn from(v: VCard) -> Self { + DavValue::VCard(v) + } +} + +impl From for DavValue { + fn from(v: SupportedLock) -> Self { + DavValue::LockEntries(v.0) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::LockEntries(List(v)) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::ActiveLocks(List(v)) + } +} + +impl From for DavValue { + fn from(v: LockDiscovery) -> Self { + DavValue::ActiveLocks(v.0) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::PrivilegeSet(List(v)) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::Privileges(List(v)) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::Href(List(v)) + } +} + +impl From> for DavValue { + fn from(v: Vec) -> Self { + DavValue::Acl(List(v)) + } +} + +impl From for DavValue { + fn from(v: AclRestrictions) -> Self { + DavValue::AclRestrictions(v) + } +} + +impl From for DavValue { + fn from(v: DeadProperty) -> Self { + DavValue::DeadProperty(v) + } +} + +impl DavPropertyValue { + pub fn new(property: impl Into, value: impl Into) -> Self { + DavPropertyValue { + property: property.into(), + value: value.into(), + } + } + + pub fn empty(property: impl Into) -> Self { + DavPropertyValue { + property: property.into(), + value: DavValue::Null, + } + } +} diff --git a/crates/dav-proto/src/responses/propstat.rs b/crates/dav-proto/src/responses/propstat.rs new file mode 100644 index 00000000..11c13334 --- /dev/null +++ b/crates/dav-proto/src/responses/propstat.rs @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use hyper::StatusCode; + +use crate::schema::{ + request::DavPropertyValue, + response::{Condition, List, Prop, PropStat, ResponseDescription, Status}, +}; + +impl Display for PropStat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + self.prop.fmt(f)?; + self.status.fmt(f)?; + if let Some(error) = &self.error { + error.fmt(f)?; + } + if let Some(response_description) = &self.response_description { + response_description.fmt(f)?; + } + write!(f, "") + } +} + +impl Display for Prop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl PropStat { + pub fn new(prop: impl Into) -> Self { + PropStat { + prop: Prop(List(vec![prop.into()])), + status: Status(StatusCode::OK), + error: None, + response_description: None, + } + } + + pub fn new_list(props: Vec) -> Self { + PropStat { + prop: Prop(List(props)), + status: Status(StatusCode::OK), + error: None, + response_description: None, + } + } + + pub fn with_prop(mut self, prop: impl Into) -> Self { + self.prop.0 .0.push(prop.into()); + self + } + + pub fn with_status(mut self, status: StatusCode) -> Self { + self.status = Status(status); + self + } + + pub fn with_error(mut self, error: impl Into) -> Self { + self.error = Some(error.into()); + self + } + + pub fn with_response_description(mut self, response_description: impl Into) -> Self { + self.response_description = Some(ResponseDescription(response_description.into())); + self + } +} diff --git a/crates/dav-proto/src/schema/mod.rs b/crates/dav-proto/src/schema/mod.rs new file mode 100644 index 00000000..e47b8aca --- /dev/null +++ b/crates/dav-proto/src/schema/mod.rs @@ -0,0 +1,1415 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::borrow::Cow; +pub mod property; +pub mod request; +pub mod response; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct NamedElement { + pub ns: Namespace, + pub element: Element, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum Namespace { + Dav, + CalDav, + CardDav, +} + +impl Namespace { + pub fn try_parse(value: &[u8]) -> Option { + hashify::tiny_map!(value, + "DAV:" => Namespace::Dav, + "urn:ietf:params:xml:ns:caldav" => Namespace::CalDav, + "urn:ietf:params:xml:ns:carddav" => Namespace::CardDav + ) + } +} + +impl AsRef for Namespace { + fn as_ref(&self) -> &str { + match self { + Namespace::Dav => "DAV:", + Namespace::CalDav => "urn:ietf:params:xml:ns:caldav", + Namespace::CardDav => "urn:ietf:params:xml:ns:carddav", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum Element { + Abstract, + Ace, + Acl, + AclPrincipalPropSet, + AclRestrictions, + Activelock, + ActivityCheckoutSet, + ActivityCollectionSet, + ActivitySet, + ActivityVersionSet, + Add, + AddMember, + AddedVersion, + AddressData, + AddressDataType, + Addressbook, + AddressbookDescription, + AddressbookHomeSet, + AddressbookMultiget, + AddressbookQuery, + After, + All, + Allcomp, + AllowClientDefinedUri, + AllowedAttendeeSchedulingObjectChange, + AllowedOrganizerSchedulingObjectChange, + AllowedPrincipal, + Allprop, + AlternateUriSet, + And, + AnyOtherProperty, + ApplyToVersion, + ApplyToPrincipalCollectionSet, + Ascending, + Authenticated, + AutoMergeSet, + AutoUpdate, + AutoVersion, + Baseline, + BaselineCollection, + BaselineControl, + BaselineControlResponse, + BaselineControlledCollection, + BaselineControlledCollectionSet, + Basicsearch, + Basicsearchschema, + Before, + Bind, + BindResponse, + BindingName, + Calendar, + CalendarAvailability, + CalendarData, + CalendarDescription, + CalendarHomeSet, + CalendarMultiget, + CalendarQuery, + CalendarTimezone, + CalendarTimezoneId, + CalendarUserAddressSet, + CalendarUserType, + Caseless, + ChangedVersion, + CheckedIn, + CheckedOut, + Checkin, + CheckinActivity, + CheckinFork, + CheckinResponse, + Checkout, + CheckoutCheckin, + CheckoutFork, + CheckoutResponse, + CheckoutSet, + CheckoutUnlockedCheckin, + Collection, + Comment, + CommonAncestor, + Comp, + CompFilter, + CompareBaseline, + CompareBaselineReport, + ConflictPreview, + Contains, + Creationdate, + CreatorDisplayname, + CurrentActivitySet, + CurrentUserPrincipal, + CurrentUserPrivilegeSet, + CurrentWorkspaceSet, + Datatype, + DefaultCalendarNeeded, + DeletedVersion, + Deny, + DenyBeforeGrant, + Depth, + Descending, + Description, + Discouraged, + Displayname, + Eq, + Error, + Exclusive, + Expand, + ExpandProperty, + Filter, + First, + Forbidden, + ForkOk, + FreeBusyQuery, + From, + Getcontentlanguage, + Getcontentlength, + Getcontenttype, + Getetag, + Getlastmodified, + Grammar, + Grant, + GrantOnly, + Group, + GroupMemberSet, + GroupMembership, + Gt, + Gte, + Href, + IgnorePreview, + Include, + IncludeVersions, + Inherited, + InheritedAclSet, + Invert, + IsCollection, + IsDefined, + IsNotDefined, + KeepCheckedOut, + Label, + LabelName, + LabelNameSet, + LabelResponse, + LanguageDefined, + LanguageMatches, + Last, + LatestActivityVersion, + LatestActivityVersionReport, + Like, + Limit, + LimitFreebusySet, + LimitRecurrenceSet, + LimitedNumberOfAces, + Literal, + LocateByHistory, + Location, + LockTokenSubmitted, + Lockdiscovery, + LockedCheckout, + Lockentry, + Lockinfo, + Lockroot, + Lockscope, + Locktoken, + Locktype, + Lt, + Lte, + ManagedAttachmentsServerUrl, + Match, + MaxAttachmentSize, + MaxAttachmentsPerResource, + MaxAttendeesPerInstance, + MaxDateTime, + MaxInstances, + MaxResourceSize, + Merge, + MergePreview, + MergePreviewReport, + MergeSet, + MinDateTime, + MissingRequiredPrincipal, + Mkactivity, + MkactivityResponse, + Mkcalendar, + MkcalendarResponse, + Mkcol, + MkcolResponse, + Mkredirectref, + MkredirectrefResponse, + Mkworkspace, + MkworkspaceResponse, + Mount, + Multistatus, + NeedPrivileges, + New, + NoAbstract, + NoAceConflict, + NoAutoMerge, + NoCheckout, + NoConflictingLock, + NoInheritedAceConflict, + NoInvert, + NoProtectedAceConflict, + NoUidConflict, + Not, + NotSupportedPrivilege, + Nresults, + Opaque, + Opdesc, + Open, + OperandLiteral, + OperandProperty, + OperandTypedLiteral, + Operators, + Options, + OptionsResponse, + Or, + Order, + OrderMember, + Orderby, + OrderingType, + Orderpatch, + OrderpatchResponse, + Owner, + ParamFilter, + Parent, + ParentSet, + Permanent, + Position, + PredecessorSet, + Principal, + PrincipalUrl, + PrincipalAddress, + PrincipalCollectionSet, + PrincipalMatch, + PrincipalProperty, + PrincipalPropertySearch, + PrincipalSearchProperty, + PrincipalSearchPropertySet, + Privilege, + Prop, + PropFilter, + Propdesc, + Properties, + Property, + PropertySearch, + Propertyupdate, + Propfind, + Propname, + Propstat, + Protected, + QuerySchema, + QuerySchemaDiscovery, + QuotaAvailableBytes, + QuotaUsedBytes, + Read, + ReadAcl, + ReadCurrentUserPrivilegeSet, + ReadFreeBusy, + Rebind, + RebindResponse, + Recipient, + RecognizedPrincipal, + RedirectLifetime, + Redirectref, + Reftarget, + Remove, + Report, + RequestStatus, + RequiredPrincipal, + Resource, + ResourceId, + Resourcetype, + Response, + Responsedescription, + RootVersion, + SameOrganizerInAllComponents, + ScheduleCalendarTransp, + ScheduleDefaultCalendarUrl, + ScheduleDeliver, + ScheduleDeliverInvite, + ScheduleDeliverReply, + ScheduleInbox, + ScheduleInboxUrl, + ScheduleOutbox, + ScheduleOutboxUrl, + ScheduleQueryFreebusy, + ScheduleResponse, + ScheduleSend, + ScheduleSendFreebusy, + ScheduleSendInvite, + ScheduleSendReply, + ScheduleTag, + Scope, + Score, + Searchable, + Segment, + Select, + Selectable, + Self_, + Set, + Shared, + Sortable, + Source, + Status, + SubactivitySet, + SubbaselineSet, + SuccessorSet, + SupportedAddressData, + SupportedCalendarComponentSet, + SupportedCalendarData, + SupportedCollation, + SupportedCollationSet, + SupportedFilter, + SupportedLiveProperty, + SupportedLivePropertySet, + SupportedMethod, + SupportedMethodSet, + SupportedPrivilege, + SupportedPrivilegeSet, + SupportedQueryGrammar, + SupportedQueryGrammarSet, + SupportedReport, + SupportedReportSet, + SupportedRscale, + SupportedRscaleSet, + Supportedlock, + SyncCollection, + SyncLevel, + SyncToken, + Target, + Temporary, + TextMatch, + TimeRange, + Timeout, + Timezone, + TimezoneId, + TimezoneServiceSet, + Transparent, + TypedLiteral, + Unauthenticated, + Unbind, + UnbindResponse, + Uncheckout, + UncheckoutResponse, + UniqueSchedulingObjectResource, + Unlock, + Unreserved, + Update, + UpdatePreview, + Updateredirectref, + UpdateredirectrefResponse, + Url, + Username, + ValidOrganizer, + ValidScheduleDefaultCalendarUrl, + ValidSchedulingMessage, + Version, + VersionControl, + VersionControlResponse, + VersionControlledBinding, + VersionControlledBindingSet, + VersionControlledConfiguration, + VersionHistory, + VersionHistoryCollectionSet, + VersionHistorySet, + VersionName, + VersionSet, + VersionTree, + Where, + Workspace, + WorkspaceCheckoutSet, + WorkspaceCollectionSet, + Write, + WriteAcl, + WriteContent, + WriteProperties, +} + +impl Element { + pub fn try_parse(value: &[u8]) -> Option<&Self> { + hashify::map!(value, + Element, + "abstract" => Element::Abstract, + "ace" => Element::Ace, + "acl" => Element::Acl, + "acl-principal-prop-set" => Element::AclPrincipalPropSet, + "acl-restrictions" => Element::AclRestrictions, + "activelock" => Element::Activelock, + "activity-checkout-set" => Element::ActivityCheckoutSet, + "activity-collection-set" => Element::ActivityCollectionSet, + "activity-set" => Element::ActivitySet, + "activity-version-set" => Element::ActivityVersionSet, + "add" => Element::Add, + "add-member" => Element::AddMember, + "added-version" => Element::AddedVersion, + "address-data" => Element::AddressData, + "address-data-type" => Element::AddressDataType, + "addressbook" => Element::Addressbook, + "addressbook-description" => Element::AddressbookDescription, + "addressbook-home-set" => Element::AddressbookHomeSet, + "addressbook-multiget" => Element::AddressbookMultiget, + "addressbook-query" => Element::AddressbookQuery, + "after" => Element::After, + "all" => Element::All, + "allcomp" => Element::Allcomp, + "allow-client-defined-uri" => Element::AllowClientDefinedUri, + "allowed-attendee-scheduling-object-change" => Element::AllowedAttendeeSchedulingObjectChange, + "allowed-organizer-scheduling-object-change" => Element::AllowedOrganizerSchedulingObjectChange, + "allowed-principal" => Element::AllowedPrincipal, + "allprop" => Element::Allprop, + "alternate-URI-set" => Element::AlternateUriSet, + "and" => Element::And, + "any-other-property" => Element::AnyOtherProperty, + "apply-to-version" => Element::ApplyToVersion, + "apply-to-principal-collection-set" => Element::ApplyToPrincipalCollectionSet, + "ascending" => Element::Ascending, + "authenticated" => Element::Authenticated, + "auto-merge-set" => Element::AutoMergeSet, + "auto-update" => Element::AutoUpdate, + "auto-version" => Element::AutoVersion, + "baseline" => Element::Baseline, + "baseline-collection" => Element::BaselineCollection, + "baseline-control" => Element::BaselineControl, + "baseline-control-response" => Element::BaselineControlResponse, + "baseline-controlled-collection" => Element::BaselineControlledCollection, + "baseline-controlled-collection-set" => Element::BaselineControlledCollectionSet, + "basicsearch" => Element::Basicsearch, + "basicsearchschema" => Element::Basicsearchschema, + "before" => Element::Before, + "bind" => Element::Bind, + "bind-response" => Element::BindResponse, + "binding-name" => Element::BindingName, + "calendar" => Element::Calendar, + "calendar-availability" => Element::CalendarAvailability, + "calendar-data" => Element::CalendarData, + "calendar-description" => Element::CalendarDescription, + "calendar-home-set" => Element::CalendarHomeSet, + "calendar-multiget" => Element::CalendarMultiget, + "calendar-query" => Element::CalendarQuery, + "calendar-timezone" => Element::CalendarTimezone, + "calendar-timezone-id" => Element::CalendarTimezoneId, + "calendar-user-address-set" => Element::CalendarUserAddressSet, + "calendar-user-type" => Element::CalendarUserType, + "caseless" => Element::Caseless, + "changed-version" => Element::ChangedVersion, + "checked-in" => Element::CheckedIn, + "checked-out" => Element::CheckedOut, + "checkin" => Element::Checkin, + "checkin-activity" => Element::CheckinActivity, + "checkin-fork" => Element::CheckinFork, + "checkin-response" => Element::CheckinResponse, + "checkout" => Element::Checkout, + "checkout-checkin" => Element::CheckoutCheckin, + "checkout-fork" => Element::CheckoutFork, + "checkout-response" => Element::CheckoutResponse, + "checkout-set" => Element::CheckoutSet, + "checkout-unlocked-checkin" => Element::CheckoutUnlockedCheckin, + "collection" => Element::Collection, + "comment" => Element::Comment, + "common-ancestor" => Element::CommonAncestor, + "comp" => Element::Comp, + "comp-filter" => Element::CompFilter, + "compare-baseline" => Element::CompareBaseline, + "compare-baseline-report" => Element::CompareBaselineReport, + "conflict-preview" => Element::ConflictPreview, + "contains" => Element::Contains, + "creationdate" => Element::Creationdate, + "creator-displayname" => Element::CreatorDisplayname, + "current-activity-set" => Element::CurrentActivitySet, + "current-user-principal" => Element::CurrentUserPrincipal, + "current-user-privilege-set" => Element::CurrentUserPrivilegeSet, + "current-workspace-set" => Element::CurrentWorkspaceSet, + "datatype" => Element::Datatype, + "default-calendar-needed" => Element::DefaultCalendarNeeded, + "deleted-version" => Element::DeletedVersion, + "deny" => Element::Deny, + "deny-before-grant" => Element::DenyBeforeGrant, + "depth" => Element::Depth, + "descending" => Element::Descending, + "description" => Element::Description, + "discouraged" => Element::Discouraged, + "displayname" => Element::Displayname, + "eq" => Element::Eq, + "error" => Element::Error, + "exclusive" => Element::Exclusive, + "expand" => Element::Expand, + "expand-property" => Element::ExpandProperty, + "filter" => Element::Filter, + "first" => Element::First, + "forbidden" => Element::Forbidden, + "fork-ok" => Element::ForkOk, + "free-busy-query" => Element::FreeBusyQuery, + "from" => Element::From, + "getcontentlanguage" => Element::Getcontentlanguage, + "getcontentlength" => Element::Getcontentlength, + "getcontenttype" => Element::Getcontenttype, + "getetag" => Element::Getetag, + "getlastmodified" => Element::Getlastmodified, + "grammar" => Element::Grammar, + "grant" => Element::Grant, + "grant-only" => Element::GrantOnly, + "group" => Element::Group, + "group-member-set" => Element::GroupMemberSet, + "group-membership" => Element::GroupMembership, + "gt" => Element::Gt, + "gte" => Element::Gte, + "href" => Element::Href, + "ignore-preview" => Element::IgnorePreview, + "include" => Element::Include, + "include-versions" => Element::IncludeVersions, + "inherited" => Element::Inherited, + "inherited-acl-set" => Element::InheritedAclSet, + "invert" => Element::Invert, + "is-collection" => Element::IsCollection, + "is-defined" => Element::IsDefined, + "is-not-defined" => Element::IsNotDefined, + "keep-checked-out" => Element::KeepCheckedOut, + "label" => Element::Label, + "label-name" => Element::LabelName, + "label-name-set" => Element::LabelNameSet, + "label-response" => Element::LabelResponse, + "language-defined" => Element::LanguageDefined, + "language-matches" => Element::LanguageMatches, + "last" => Element::Last, + "latest-activity-version" => Element::LatestActivityVersion, + "latest-activity-version-report" => Element::LatestActivityVersionReport, + "like" => Element::Like, + "limit" => Element::Limit, + "limit-freebusy-set" => Element::LimitFreebusySet, + "limit-recurrence-set" => Element::LimitRecurrenceSet, + "limited-number-of-aces" => Element::LimitedNumberOfAces, + "literal" => Element::Literal, + "locate-by-history" => Element::LocateByHistory, + "location" => Element::Location, + "lock-token-submitted" => Element::LockTokenSubmitted, + "lockdiscovery" => Element::Lockdiscovery, + "locked-checkout" => Element::LockedCheckout, + "lockentry" => Element::Lockentry, + "lockinfo" => Element::Lockinfo, + "lockroot" => Element::Lockroot, + "lockscope" => Element::Lockscope, + "locktoken" => Element::Locktoken, + "locktype" => Element::Locktype, + "lt" => Element::Lt, + "lte" => Element::Lte, + "managed-attachments-server-URL" => Element::ManagedAttachmentsServerUrl, + "match" => Element::Match, + "max-attachment-size" => Element::MaxAttachmentSize, + "max-attachments-per-resource" => Element::MaxAttachmentsPerResource, + "max-attendees-per-instance" => Element::MaxAttendeesPerInstance, + "max-date-time" => Element::MaxDateTime, + "max-instances" => Element::MaxInstances, + "max-resource-size" => Element::MaxResourceSize, + "merge" => Element::Merge, + "merge-preview" => Element::MergePreview, + "merge-preview-report" => Element::MergePreviewReport, + "merge-set" => Element::MergeSet, + "min-date-time" => Element::MinDateTime, + "missing-required-principal" => Element::MissingRequiredPrincipal, + "mkactivity" => Element::Mkactivity, + "mkactivity-response" => Element::MkactivityResponse, + "mkcalendar" => Element::Mkcalendar, + "mkcalendar-response" => Element::MkcalendarResponse, + "mkcol" => Element::Mkcol, + "mkcol-response" => Element::MkcolResponse, + "mkredirectref" => Element::Mkredirectref, + "mkredirectref-response" => Element::MkredirectrefResponse, + "mkworkspace" => Element::Mkworkspace, + "mkworkspace-response" => Element::MkworkspaceResponse, + "mount" => Element::Mount, + "multistatus" => Element::Multistatus, + "need-privileges" => Element::NeedPrivileges, + "new" => Element::New, + "no-abstract" => Element::NoAbstract, + "no-ace-conflict" => Element::NoAceConflict, + "no-auto-merge" => Element::NoAutoMerge, + "no-checkout" => Element::NoCheckout, + "no-conflicting-lock" => Element::NoConflictingLock, + "no-inherited-ace-conflict" => Element::NoInheritedAceConflict, + "no-invert" => Element::NoInvert, + "no-protected-ace-conflict" => Element::NoProtectedAceConflict, + "no-uid-conflict" => Element::NoUidConflict, + "not" => Element::Not, + "not-supported-privilege" => Element::NotSupportedPrivilege, + "nresults" => Element::Nresults, + "opaque" => Element::Opaque, + "opdesc" => Element::Opdesc, + "open" => Element::Open, + "operand-literal" => Element::OperandLiteral, + "operand-property" => Element::OperandProperty, + "operand-typed-literal" => Element::OperandTypedLiteral, + "operators" => Element::Operators, + "options" => Element::Options, + "options-response" => Element::OptionsResponse, + "or" => Element::Or, + "order" => Element::Order, + "order-member" => Element::OrderMember, + "orderby" => Element::Orderby, + "ordering-type" => Element::OrderingType, + "orderpatch" => Element::Orderpatch, + "orderpatch-response" => Element::OrderpatchResponse, + "owner" => Element::Owner, + "param-filter" => Element::ParamFilter, + "parent" => Element::Parent, + "parent-set" => Element::ParentSet, + "permanent" => Element::Permanent, + "position" => Element::Position, + "predecessor-set" => Element::PredecessorSet, + "principal" => Element::Principal, + "principal-URL" => Element::PrincipalUrl, + "principal-address" => Element::PrincipalAddress, + "principal-collection-set" => Element::PrincipalCollectionSet, + "principal-match" => Element::PrincipalMatch, + "principal-property" => Element::PrincipalProperty, + "principal-property-search" => Element::PrincipalPropertySearch, + "principal-search-property" => Element::PrincipalSearchProperty, + "principal-search-property-set" => Element::PrincipalSearchPropertySet, + "privilege" => Element::Privilege, + "prop" => Element::Prop, + "prop-filter" => Element::PropFilter, + "propdesc" => Element::Propdesc, + "properties" => Element::Properties, + "property" => Element::Property, + "property-search" => Element::PropertySearch, + "propertyupdate" => Element::Propertyupdate, + "propfind" => Element::Propfind, + "propname" => Element::Propname, + "propstat" => Element::Propstat, + "protected" => Element::Protected, + "query-schema" => Element::QuerySchema, + "query-schema-discovery" => Element::QuerySchemaDiscovery, + "quota-available-bytes" => Element::QuotaAvailableBytes, + "quota-used-bytes" => Element::QuotaUsedBytes, + "read" => Element::Read, + "read-acl" => Element::ReadAcl, + "read-current-user-privilege-set" => Element::ReadCurrentUserPrivilegeSet, + "read-free-busy" => Element::ReadFreeBusy, + "rebind" => Element::Rebind, + "rebind-response" => Element::RebindResponse, + "recipient" => Element::Recipient, + "recognized-principal" => Element::RecognizedPrincipal, + "redirect-lifetime" => Element::RedirectLifetime, + "redirectref" => Element::Redirectref, + "reftarget" => Element::Reftarget, + "remove" => Element::Remove, + "report" => Element::Report, + "request-status" => Element::RequestStatus, + "required-principal" => Element::RequiredPrincipal, + "resource" => Element::Resource, + "resource-id" => Element::ResourceId, + "resourcetype" => Element::Resourcetype, + "response" => Element::Response, + "responsedescription" => Element::Responsedescription, + "root-version" => Element::RootVersion, + "same-organizer-in-all-components" => Element::SameOrganizerInAllComponents, + "schedule-calendar-transp" => Element::ScheduleCalendarTransp, + "schedule-default-calendar-URL" => Element::ScheduleDefaultCalendarUrl, + "schedule-deliver" => Element::ScheduleDeliver, + "schedule-deliver-invite" => Element::ScheduleDeliverInvite, + "schedule-deliver-reply" => Element::ScheduleDeliverReply, + "schedule-inbox" => Element::ScheduleInbox, + "schedule-inbox-URL" => Element::ScheduleInboxUrl, + "schedule-outbox" => Element::ScheduleOutbox, + "schedule-outbox-URL" => Element::ScheduleOutboxUrl, + "schedule-query-freebusy" => Element::ScheduleQueryFreebusy, + "schedule-response" => Element::ScheduleResponse, + "schedule-send" => Element::ScheduleSend, + "schedule-send-freebusy" => Element::ScheduleSendFreebusy, + "schedule-send-invite" => Element::ScheduleSendInvite, + "schedule-send-reply" => Element::ScheduleSendReply, + "schedule-tag" => Element::ScheduleTag, + "scope" => Element::Scope, + "score" => Element::Score, + "searchable" => Element::Searchable, + "segment" => Element::Segment, + "select" => Element::Select, + "selectable" => Element::Selectable, + "self" => Element::Self_, + "set" => Element::Set, + "shared" => Element::Shared, + "sortable" => Element::Sortable, + "source" => Element::Source, + "status" => Element::Status, + "subactivity-set" => Element::SubactivitySet, + "subbaseline-set" => Element::SubbaselineSet, + "successor-set" => Element::SuccessorSet, + "supported-address-data" => Element::SupportedAddressData, + "supported-calendar-component-set" => Element::SupportedCalendarComponentSet, + "supported-calendar-data" => Element::SupportedCalendarData, + "supported-collation" => Element::SupportedCollation, + "supported-collation-set" => Element::SupportedCollationSet, + "supported-filter" => Element::SupportedFilter, + "supported-live-property" => Element::SupportedLiveProperty, + "supported-live-property-set" => Element::SupportedLivePropertySet, + "supported-method" => Element::SupportedMethod, + "supported-method-set" => Element::SupportedMethodSet, + "supported-privilege" => Element::SupportedPrivilege, + "supported-privilege-set" => Element::SupportedPrivilegeSet, + "supported-query-grammar" => Element::SupportedQueryGrammar, + "supported-query-grammar-set" => Element::SupportedQueryGrammarSet, + "supported-report" => Element::SupportedReport, + "supported-report-set" => Element::SupportedReportSet, + "supported-rscale" => Element::SupportedRscale, + "supported-rscale-set" => Element::SupportedRscaleSet, + "supportedlock" => Element::Supportedlock, + "sync-collection" => Element::SyncCollection, + "sync-level" => Element::SyncLevel, + "sync-token" => Element::SyncToken, + "target" => Element::Target, + "temporary" => Element::Temporary, + "text-match" => Element::TextMatch, + "time-range" => Element::TimeRange, + "timeout" => Element::Timeout, + "timezone" => Element::Timezone, + "timezone-id" => Element::TimezoneId, + "timezone-service-set" => Element::TimezoneServiceSet, + "transparent" => Element::Transparent, + "typed-literal" => Element::TypedLiteral, + "unauthenticated" => Element::Unauthenticated, + "unbind" => Element::Unbind, + "unbind-response" => Element::UnbindResponse, + "uncheckout" => Element::Uncheckout, + "uncheckout-response" => Element::UncheckoutResponse, + "unique-scheduling-object-resource" => Element::UniqueSchedulingObjectResource, + "unlock" => Element::Unlock, + "unreserved" => Element::Unreserved, + "update" => Element::Update, + "update-preview" => Element::UpdatePreview, + "updateredirectref" => Element::Updateredirectref, + "updateredirectref-response" => Element::UpdateredirectrefResponse, + "url" => Element::Url, + "username" => Element::Username, + "valid-organizer" => Element::ValidOrganizer, + "valid-schedule-default-calendar-URL" => Element::ValidScheduleDefaultCalendarUrl, + "valid-scheduling-message" => Element::ValidSchedulingMessage, + "version" => Element::Version, + "version-control" => Element::VersionControl, + "version-control-response" => Element::VersionControlResponse, + "version-controlled-binding" => Element::VersionControlledBinding, + "version-controlled-binding-set" => Element::VersionControlledBindingSet, + "version-controlled-configuration" => Element::VersionControlledConfiguration, + "version-history" => Element::VersionHistory, + "version-history-collection-set" => Element::VersionHistoryCollectionSet, + "version-history-set" => Element::VersionHistorySet, + "version-name" => Element::VersionName, + "version-set" => Element::VersionSet, + "version-tree" => Element::VersionTree, + "where" => Element::Where, + "workspace" => Element::Workspace, + "workspace-checkout-set" => Element::WorkspaceCheckoutSet, + "workspace-collection-set" => Element::WorkspaceCollectionSet, + "write" => Element::Write, + "write-acl" => Element::WriteAcl, + "write-content" => Element::WriteContent, + "write-properties" => Element::WriteProperties, + ) + } +} + +impl AsRef for Element { + fn as_ref(&self) -> &str { + match self { + Element::Abstract => "abstract", + Element::Ace => "ace", + Element::Acl => "acl", + Element::AclPrincipalPropSet => "acl-principal-prop-set", + Element::AclRestrictions => "acl-restrictions", + Element::Activelock => "activelock", + Element::ActivityCheckoutSet => "activity-checkout-set", + Element::ActivityCollectionSet => "activity-collection-set", + Element::ActivitySet => "activity-set", + Element::ActivityVersionSet => "activity-version-set", + Element::Add => "add", + Element::AddMember => "add-member", + Element::AddedVersion => "added-version", + Element::AddressData => "address-data", + Element::AddressDataType => "address-data-type", + Element::Addressbook => "addressbook", + Element::AddressbookDescription => "addressbook-description", + Element::AddressbookHomeSet => "addressbook-home-set", + Element::AddressbookMultiget => "addressbook-multiget", + Element::AddressbookQuery => "addressbook-query", + Element::After => "after", + Element::All => "all", + Element::Allcomp => "allcomp", + Element::AllowClientDefinedUri => "allow-client-defined-uri", + Element::AllowedAttendeeSchedulingObjectChange => { + "allowed-attendee-scheduling-object-change" + } + Element::AllowedOrganizerSchedulingObjectChange => { + "allowed-organizer-scheduling-object-change" + } + Element::AllowedPrincipal => "allowed-principal", + Element::Allprop => "allprop", + Element::AlternateUriSet => "alternate-URI-set", + Element::And => "and", + Element::AnyOtherProperty => "any-other-property", + Element::ApplyToVersion => "apply-to-version", + Element::ApplyToPrincipalCollectionSet => "apply-to-principal-collection-set", + Element::Ascending => "ascending", + Element::Authenticated => "authenticated", + Element::AutoMergeSet => "auto-merge-set", + Element::AutoUpdate => "auto-update", + Element::AutoVersion => "auto-version", + Element::Baseline => "baseline", + Element::BaselineCollection => "baseline-collection", + Element::BaselineControl => "baseline-control", + Element::BaselineControlResponse => "baseline-control-response", + Element::BaselineControlledCollection => "baseline-controlled-collection", + Element::BaselineControlledCollectionSet => "baseline-controlled-collection-set", + Element::Basicsearch => "basicsearch", + Element::Basicsearchschema => "basicsearchschema", + Element::Before => "before", + Element::Bind => "bind", + Element::BindResponse => "bind-response", + Element::BindingName => "binding-name", + Element::Calendar => "calendar", + Element::CalendarAvailability => "calendar-availability", + Element::CalendarData => "calendar-data", + Element::CalendarDescription => "calendar-description", + Element::CalendarHomeSet => "calendar-home-set", + Element::CalendarMultiget => "calendar-multiget", + Element::CalendarQuery => "calendar-query", + Element::CalendarTimezone => "calendar-timezone", + Element::CalendarTimezoneId => "calendar-timezone-id", + Element::CalendarUserAddressSet => "calendar-user-address-set", + Element::CalendarUserType => "calendar-user-type", + Element::Caseless => "caseless", + Element::ChangedVersion => "changed-version", + Element::CheckedIn => "checked-in", + Element::CheckedOut => "checked-out", + Element::Checkin => "checkin", + Element::CheckinActivity => "checkin-activity", + Element::CheckinFork => "checkin-fork", + Element::CheckinResponse => "checkin-response", + Element::Checkout => "checkout", + Element::CheckoutCheckin => "checkout-checkin", + Element::CheckoutFork => "checkout-fork", + Element::CheckoutResponse => "checkout-response", + Element::CheckoutSet => "checkout-set", + Element::CheckoutUnlockedCheckin => "checkout-unlocked-checkin", + Element::Collection => "collection", + Element::Comment => "comment", + Element::CommonAncestor => "common-ancestor", + Element::Comp => "comp", + Element::CompFilter => "comp-filter", + Element::CompareBaseline => "compare-baseline", + Element::CompareBaselineReport => "compare-baseline-report", + Element::ConflictPreview => "conflict-preview", + Element::Contains => "contains", + Element::Creationdate => "creationdate", + Element::CreatorDisplayname => "creator-displayname", + Element::CurrentActivitySet => "current-activity-set", + Element::CurrentUserPrincipal => "current-user-principal", + Element::CurrentUserPrivilegeSet => "current-user-privilege-set", + Element::CurrentWorkspaceSet => "current-workspace-set", + Element::Datatype => "datatype", + Element::DefaultCalendarNeeded => "default-calendar-needed", + Element::DeletedVersion => "deleted-version", + Element::Deny => "deny", + Element::DenyBeforeGrant => "deny-before-grant", + Element::Depth => "depth", + Element::Descending => "descending", + Element::Description => "description", + Element::Discouraged => "discouraged", + Element::Displayname => "displayname", + Element::Eq => "eq", + Element::Error => "error", + Element::Exclusive => "exclusive", + Element::Expand => "expand", + Element::ExpandProperty => "expand-property", + Element::Filter => "filter", + Element::First => "first", + Element::Forbidden => "forbidden", + Element::ForkOk => "fork-ok", + Element::FreeBusyQuery => "free-busy-query", + Element::From => "from", + Element::Getcontentlanguage => "getcontentlanguage", + Element::Getcontentlength => "getcontentlength", + Element::Getcontenttype => "getcontenttype", + Element::Getetag => "getetag", + Element::Getlastmodified => "getlastmodified", + Element::Grammar => "grammar", + Element::Grant => "grant", + Element::GrantOnly => "grant-only", + Element::Group => "group", + Element::GroupMemberSet => "group-member-set", + Element::GroupMembership => "group-membership", + Element::Gt => "gt", + Element::Gte => "gte", + Element::Href => "href", + Element::IgnorePreview => "ignore-preview", + Element::Include => "include", + Element::IncludeVersions => "include-versions", + Element::Inherited => "inherited", + Element::InheritedAclSet => "inherited-acl-set", + Element::Invert => "invert", + Element::IsCollection => "is-collection", + Element::IsDefined => "is-defined", + Element::IsNotDefined => "is-not-defined", + Element::KeepCheckedOut => "keep-checked-out", + Element::Label => "label", + Element::LabelName => "label-name", + Element::LabelNameSet => "label-name-set", + Element::LabelResponse => "label-response", + Element::LanguageDefined => "language-defined", + Element::LanguageMatches => "language-matches", + Element::Last => "last", + Element::LatestActivityVersion => "latest-activity-version", + Element::LatestActivityVersionReport => "latest-activity-version-report", + Element::Like => "like", + Element::Limit => "limit", + Element::LimitFreebusySet => "limit-freebusy-set", + Element::LimitRecurrenceSet => "limit-recurrence-set", + Element::LimitedNumberOfAces => "limited-number-of-aces", + Element::Literal => "literal", + Element::LocateByHistory => "locate-by-history", + Element::Location => "location", + Element::LockTokenSubmitted => "lock-token-submitted", + Element::Lockdiscovery => "lockdiscovery", + Element::LockedCheckout => "locked-checkout", + Element::Lockentry => "lockentry", + Element::Lockinfo => "lockinfo", + Element::Lockroot => "lockroot", + Element::Lockscope => "lockscope", + Element::Locktoken => "locktoken", + Element::Locktype => "locktype", + Element::Lt => "lt", + Element::Lte => "lte", + Element::ManagedAttachmentsServerUrl => "managed-attachments-server-URL", + Element::Match => "match", + Element::MaxAttachmentSize => "max-attachment-size", + Element::MaxAttachmentsPerResource => "max-attachments-per-resource", + Element::MaxAttendeesPerInstance => "max-attendees-per-instance", + Element::MaxDateTime => "max-date-time", + Element::MaxInstances => "max-instances", + Element::MaxResourceSize => "max-resource-size", + Element::Merge => "merge", + Element::MergePreview => "merge-preview", + Element::MergePreviewReport => "merge-preview-report", + Element::MergeSet => "merge-set", + Element::MinDateTime => "min-date-time", + Element::MissingRequiredPrincipal => "missing-required-principal", + Element::Mkactivity => "mkactivity", + Element::MkactivityResponse => "mkactivity-response", + Element::Mkcalendar => "mkcalendar", + Element::MkcalendarResponse => "mkcalendar-response", + Element::Mkcol => "mkcol", + Element::MkcolResponse => "mkcol-response", + Element::Mkredirectref => "mkredirectref", + Element::MkredirectrefResponse => "mkredirectref-response", + Element::Mkworkspace => "mkworkspace", + Element::MkworkspaceResponse => "mkworkspace-response", + Element::Mount => "mount", + Element::Multistatus => "multistatus", + Element::NeedPrivileges => "need-privileges", + Element::New => "new", + Element::NoAbstract => "no-abstract", + Element::NoAceConflict => "no-ace-conflict", + Element::NoAutoMerge => "no-auto-merge", + Element::NoCheckout => "no-checkout", + Element::NoConflictingLock => "no-conflicting-lock", + Element::NoInheritedAceConflict => "no-inherited-ace-conflict", + Element::NoInvert => "no-invert", + Element::NoProtectedAceConflict => "no-protected-ace-conflict", + Element::NoUidConflict => "no-uid-conflict", + Element::Not => "not", + Element::NotSupportedPrivilege => "not-supported-privilege", + Element::Nresults => "nresults", + Element::Opaque => "opaque", + Element::Opdesc => "opdesc", + Element::Open => "open", + Element::OperandLiteral => "operand-literal", + Element::OperandProperty => "operand-property", + Element::OperandTypedLiteral => "operand-typed-literal", + Element::Operators => "operators", + Element::Options => "options", + Element::OptionsResponse => "options-response", + Element::Or => "or", + Element::Order => "order", + Element::OrderMember => "order-member", + Element::Orderby => "orderby", + Element::OrderingType => "ordering-type", + Element::Orderpatch => "orderpatch", + Element::OrderpatchResponse => "orderpatch-response", + Element::Owner => "owner", + Element::ParamFilter => "param-filter", + Element::Parent => "parent", + Element::ParentSet => "parent-set", + Element::Permanent => "permanent", + Element::Position => "position", + Element::PredecessorSet => "predecessor-set", + Element::Principal => "principal", + Element::PrincipalUrl => "principal-URL", + Element::PrincipalAddress => "principal-address", + Element::PrincipalCollectionSet => "principal-collection-set", + Element::PrincipalMatch => "principal-match", + Element::PrincipalProperty => "principal-property", + Element::PrincipalPropertySearch => "principal-property-search", + Element::PrincipalSearchProperty => "principal-search-property", + Element::PrincipalSearchPropertySet => "principal-search-property-set", + Element::Privilege => "privilege", + Element::Prop => "prop", + Element::PropFilter => "prop-filter", + Element::Propdesc => "propdesc", + Element::Properties => "properties", + Element::Property => "property", + Element::PropertySearch => "property-search", + Element::Propertyupdate => "propertyupdate", + Element::Propfind => "propfind", + Element::Propname => "propname", + Element::Propstat => "propstat", + Element::Protected => "protected", + Element::QuerySchema => "query-schema", + Element::QuerySchemaDiscovery => "query-schema-discovery", + Element::QuotaAvailableBytes => "quota-available-bytes", + Element::QuotaUsedBytes => "quota-used-bytes", + Element::Read => "read", + Element::ReadAcl => "read-acl", + Element::ReadCurrentUserPrivilegeSet => "read-current-user-privilege-set", + Element::ReadFreeBusy => "read-free-busy", + Element::Rebind => "rebind", + Element::RebindResponse => "rebind-response", + Element::Recipient => "recipient", + Element::RecognizedPrincipal => "recognized-principal", + Element::RedirectLifetime => "redirect-lifetime", + Element::Redirectref => "redirectref", + Element::Reftarget => "reftarget", + Element::Remove => "remove", + Element::Report => "report", + Element::RequestStatus => "request-status", + Element::RequiredPrincipal => "required-principal", + Element::Resource => "resource", + Element::ResourceId => "resource-id", + Element::Resourcetype => "resourcetype", + Element::Response => "response", + Element::Responsedescription => "responsedescription", + Element::RootVersion => "root-version", + Element::SameOrganizerInAllComponents => "same-organizer-in-all-components", + Element::ScheduleCalendarTransp => "schedule-calendar-transp", + Element::ScheduleDefaultCalendarUrl => "schedule-default-calendar-URL", + Element::ScheduleDeliver => "schedule-deliver", + Element::ScheduleDeliverInvite => "schedule-deliver-invite", + Element::ScheduleDeliverReply => "schedule-deliver-reply", + Element::ScheduleInbox => "schedule-inbox", + Element::ScheduleInboxUrl => "schedule-inbox-URL", + Element::ScheduleOutbox => "schedule-outbox", + Element::ScheduleOutboxUrl => "schedule-outbox-URL", + Element::ScheduleQueryFreebusy => "schedule-query-freebusy", + Element::ScheduleResponse => "schedule-response", + Element::ScheduleSend => "schedule-send", + Element::ScheduleSendFreebusy => "schedule-send-freebusy", + Element::ScheduleSendInvite => "schedule-send-invite", + Element::ScheduleSendReply => "schedule-send-reply", + Element::ScheduleTag => "schedule-tag", + Element::Scope => "scope", + Element::Score => "score", + Element::Searchable => "searchable", + Element::Segment => "segment", + Element::Select => "select", + Element::Selectable => "selectable", + Element::Self_ => "self", + Element::Set => "set", + Element::Shared => "shared", + Element::Sortable => "sortable", + Element::Source => "source", + Element::Status => "status", + Element::SubactivitySet => "subactivity-set", + Element::SubbaselineSet => "subbaseline-set", + Element::SuccessorSet => "successor-set", + Element::SupportedAddressData => "supported-address-data", + Element::SupportedCalendarComponentSet => "supported-calendar-component-set", + Element::SupportedCalendarData => "supported-calendar-data", + Element::SupportedCollation => "supported-collation", + Element::SupportedCollationSet => "supported-collation-set", + Element::SupportedFilter => "supported-filter", + Element::SupportedLiveProperty => "supported-live-property", + Element::SupportedLivePropertySet => "supported-live-property-set", + Element::SupportedMethod => "supported-method", + Element::SupportedMethodSet => "supported-method-set", + Element::SupportedPrivilege => "supported-privilege", + Element::SupportedPrivilegeSet => "supported-privilege-set", + Element::SupportedQueryGrammar => "supported-query-grammar", + Element::SupportedQueryGrammarSet => "supported-query-grammar-set", + Element::SupportedReport => "supported-report", + Element::SupportedReportSet => "supported-report-set", + Element::SupportedRscale => "supported-rscale", + Element::SupportedRscaleSet => "supported-rscale-set", + Element::Supportedlock => "supportedlock", + Element::SyncCollection => "sync-collection", + Element::SyncLevel => "sync-level", + Element::SyncToken => "sync-token", + Element::Target => "target", + Element::Temporary => "temporary", + Element::TextMatch => "text-match", + Element::TimeRange => "time-range", + Element::Timeout => "timeout", + Element::Timezone => "timezone", + Element::TimezoneId => "timezone-id", + Element::TimezoneServiceSet => "timezone-service-set", + Element::Transparent => "transparent", + Element::TypedLiteral => "typed-literal", + Element::Unauthenticated => "unauthenticated", + Element::Unbind => "unbind", + Element::UnbindResponse => "unbind-response", + Element::Uncheckout => "uncheckout", + Element::UncheckoutResponse => "uncheckout-response", + Element::UniqueSchedulingObjectResource => "unique-scheduling-object-resource", + Element::Unlock => "unlock", + Element::Unreserved => "unreserved", + Element::Update => "update", + Element::UpdatePreview => "update-preview", + Element::Updateredirectref => "updateredirectref", + Element::UpdateredirectrefResponse => "updateredirectref-response", + Element::Url => "url", + Element::Username => "username", + Element::ValidOrganizer => "valid-organizer", + Element::ValidScheduleDefaultCalendarUrl => "valid-schedule-default-calendar-URL", + Element::ValidSchedulingMessage => "valid-scheduling-message", + Element::Version => "version", + Element::VersionControl => "version-control", + Element::VersionControlResponse => "version-control-response", + Element::VersionControlledBinding => "version-controlled-binding", + Element::VersionControlledBindingSet => "version-controlled-binding-set", + Element::VersionControlledConfiguration => "version-controlled-configuration", + Element::VersionHistory => "version-history", + Element::VersionHistoryCollectionSet => "version-history-collection-set", + Element::VersionHistorySet => "version-history-set", + Element::VersionName => "version-name", + Element::VersionSet => "version-set", + Element::VersionTree => "version-tree", + Element::Where => "where", + Element::Workspace => "workspace", + Element::WorkspaceCheckoutSet => "workspace-checkout-set", + Element::WorkspaceCollectionSet => "workspace-collection-set", + Element::Write => "write", + Element::WriteAcl => "write-acl", + Element::WriteContent => "write-content", + Element::WriteProperties => "write-properties", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Attribute { + Caseless(bool), + XsiType(XsiType), + AllowPCData(bool), + Name(T), + Namespace(Namespace), + ContentType(String), + XmlLanguage(String), + Version(String), + NoValue(bool), + TestAllOf(bool), + MatchType(MatchType), + NegateCondition(bool), + Collation(Collation), + Start(T), + End(T), + Unknown { param: String, value: String }, +} + +pub trait AttributeValue { + fn from_str(s: &str) -> Option + where + Self: Sized; +} + +impl Attribute { + pub fn from_param(param: &[u8], value: Cow<'_, str>) -> Option> { + hashify::fnc_map!(param, + "caseless" => { + if let Some(b) = YesNo::from_str(value.as_ref()) { + return Some(Attribute::Caseless(b)); + } + }, + "xsi:type" => { + return Some(Attribute::XsiType(XsiType::from_str(value.as_ref()).unwrap_or(XsiType::Unsupported))); + }, + "allow-pcdata" => { + if let Some(b) = YesNo::from_str(value.as_ref()) { + return Some(Attribute::AllowPCData(b)); + } + }, + "novalue" => { + if let Some(b) = YesNo::from_str(value.as_ref()) { + return Some(Attribute::NoValue(b)); + } + }, + "negate-condition" => { + if let Some(b) = YesNo::from_str(value.as_ref()) { + return Some(Attribute::NegateCondition(b)); + } + }, + "name" => { + if let Some(value) = T::from_str(value.as_ref()) { + return Some(Attribute::Name(value)); + } + }, + "namespace" => { + if let Some(ns) = Namespace::try_parse(value.as_bytes()) { + return Some(Attribute::Namespace(ns)); + } + }, + "content-type" => { + return Some(Attribute::ContentType(value.into_owned())); + }, + "version" => { + return Some(Attribute::Version(value.into_owned())); + }, + "test" => { + return Some(Attribute::TestAllOf(value.eq("allof"))); + }, + "match-type" => { + if let Some(mt) = MatchType::try_parse(value.as_ref()) { + return Some(Attribute::MatchType(mt)); + } + }, + "collation" => { + if let Some(c) = Collation::try_parse(value.as_ref()) { + return Some(Attribute::Collation(c)); + } + }, + "start" => { + if let Some(value) = T::from_str(value.as_ref()) { + return Some(Attribute::Start(value)); + } + }, + "end" => { + if let Some(value) = T::from_str(value.as_ref()) { + return Some(Attribute::End(value)); + } + }, + "xml:lang" => { + return Some(Attribute::XmlLanguage(value.into_owned())); + }, + "xmlns" => { + return None; + }, + _ => { + if param.starts_with(b"xmlns:") { + return None; + } + } + ); + + Some(Attribute::Unknown { + param: String::from_utf8_lossy(param).into_owned(), + value: value.into_owned(), + }) + } +} + +impl AttributeValue for String { + fn from_str(s: &str) -> Option { + Some(s.to_string()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum Collation { + AsciiNumeric, + AsciiCasemap, + Octet, + UnicodeCasemap, +} + +impl Collation { + pub fn try_parse(s: &str) -> Option { + hashify::tiny_map!(s.as_bytes(), + "i;ascii-numeric" => Collation::AsciiNumeric, + "i;ascii-casemap" => Collation::AsciiCasemap, + "i;octet" => Collation::Octet, + "i;unicode-casemap" => Collation::UnicodeCasemap, + ) + } + + pub fn as_str(&self) -> &'static str { + match self { + Collation::AsciiNumeric => "i;ascii-numeric", + Collation::AsciiCasemap => "i;ascii-casemap", + Collation::Octet => "i;octet", + Collation::UnicodeCasemap => "i;unicode-casemap", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum MatchType { + Equals, + Contains, + StartsWith, + EndsWith, +} + +impl MatchType { + pub fn try_parse(s: &str) -> Option { + hashify::tiny_map!(s.as_bytes(), + "equals" => MatchType::Equals, + "contains" => MatchType::Contains, + "starts-with" => MatchType::StartsWith, + "ends-with" => MatchType::EndsWith, + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum XsiType { + String, + Boolean, + Decimal, + Float, + Double, + Duration, + DateTime, + Time, + Date, + GYearMonth, + GYear, + GMonthDay, + GDay, + GMonth, + HexBinary, + Base64Binary, + AnyUri, + QName, + Notation, + Unsupported, +} + +impl XsiType { + fn from_str(s: &str) -> Option { + hashify::tiny_map!(s.as_bytes(), + "xs:string" => XsiType::String, + "xs:boolean" => XsiType::Boolean, + "xs:decimal" => XsiType::Decimal, + "xs:float" => XsiType::Float, + "xs:double" => XsiType::Double, + "xs:duration" => XsiType::Duration, + "xs:dateTime" => XsiType::DateTime, + "xs:time" => XsiType::Time, + "xs:date" => XsiType::Date, + "xs:gYearMonth" => XsiType::GYearMonth, + "xs:gYear" => XsiType::GYear, + "xs:gMonthDay" => XsiType::GMonthDay, + "xs:gDay" => XsiType::GDay, + "xs:gMonth" => XsiType::GMonth, + "xs:hexBinary" => XsiType::HexBinary, + "xs:base64Binary" => XsiType::Base64Binary, + "xs:anyURI" => XsiType::AnyUri, + "xs:QName" => XsiType::QName, + "xs:NOTATION" => XsiType::Notation, + ) + } +} + +struct YesNo; + +impl YesNo { + fn from_str(s: &str) -> Option { + hashify::tiny_map!(s.as_bytes(), + "yes" => true, + "no" => false, + ) + } +} diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs new file mode 100644 index 00000000..e3531369 --- /dev/null +++ b/crates/dav-proto/src/schema/property.rs @@ -0,0 +1,279 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use calcard::{ + icalendar::{ICalendar, ICalendarComponentType, ICalendarProperty}, + vcard::{VCard, VCardProperty}, +}; + +use crate::{Depth, Timeout}; + +use super::{ + request::{DavPropertyValue, DeadElementTag, DeadProperty}, + response::{Ace, AclRestrictions, Href, List, SupportedPrivilege}, + Collation, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum DavProperty { + WebDav(WebDavProperty), + CardDav(CardDavProperty), + CalDav(CalDavProperty), + DeadProperty(DeadElementTag), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum WebDavProperty { + CreationDate, + DisplayName, + GetContentLanguage, + GetContentLength, + GetContentType, + GetETag, + GetLastModified, + ResourceType, + LockDiscovery, + SupportedLock, + SupportedReportSet, + CurrentUserPrincipal, + // Quota properties + QuotaAvailableBytes, + QuotaUsedBytes, + // Sync properties + SyncToken, + // Principal properties + AlternateURISet, + PrincipalURL, + GroupMemberSet, + GroupMembership, + // ACL properties (all protected) + Owner, + Group, + SupportedPrivilegeSet, + CurrentUserPrivilegeSet, + Acl, + AclRestrictions, + InheritedAclSet, + PrincipalCollectionSet, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum CardDavProperty { + AddressbookDescription, + SupportedAddressData, + SupportedCollationSet, + MaxResourceSize, + AddressData(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct CardDavPropertyName { + pub group: Option, + pub name: VCardProperty, + pub no_value: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum CalDavProperty { + CalendarDescription, + CalendarTimezone, + SupportedCalendarComponentSet, + SupportedCalendarData, + SupportedCollationSet, + MaxResourceSize, + MinDateTime, + MaxDateTime, + MaxInstances, + MaxAttendeesPerInstance, + CalendarHomeSet, + CalendarData(CalendarData), + TimezoneServiceSet, + TimezoneId, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct CalendarData { + pub properties: Vec, + pub expand: Option, + pub limit_recurrence: Option, + pub limit_freebusy: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct DateRange { + pub start: i64, + pub end: i64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct CalDavPropertyName { + pub component: Option, + pub name: Option, + pub no_value: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[repr(transparent)] +pub struct Rfc1123DateTime(pub(crate) i64); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum DavValue { + Timestamp(i64), + Rfc1123Date(Rfc1123DateTime), + Uint64(u64), + String(String), + ResourceTypes(List), + ActiveLocks(List), + LockEntries(List), + ReportSets(List), + ICalendar(ICalendar), + VCard(VCard), + Components(List), + Collations(List), + PrivilegeSet(List), + Privileges(List), + Href(List), + Acl(List), + AclRestrictions(AclRestrictions), + DeadProperty(DeadProperty), + Null, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum ReportSet { + SyncCollection, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct Comp(pub ICalendarComponentType); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct SupportedCollation(pub Collation); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum ResourceType { + Collection, + Principal, + AddressBook, + Calendar, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct LockDiscovery(pub List); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct ActiveLock { + pub lock_scope: LockScope, + pub lock_type: LockType, + pub depth: Depth, + pub owner: Option, + pub timeout: Timeout, + pub lock_token: Option, + pub lock_root: Href, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct SupportedLock(pub List); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct LockEntry { + pub lock_scope: LockScope, + pub lock_type: LockType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum LockType { + Write, + Other, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum LockScope { + Exclusive, + Shared, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum Privilege { + Read, + Write, + WriteProperties, + WriteContent, + Unlock, + ReadAcl, + ReadCurrentUserPrivilegeSet, + WriteAcl, + Bind, + Unbind, + All, + ReadFreeBusy, +} + +impl From for DavPropertyValue { + fn from(value: DavProperty) -> Self { + DavPropertyValue { + property: value, + value: DavValue::Null, + } + } +} + +impl Rfc1123DateTime { + pub fn new(timestamp: i64) -> Self { + Self(timestamp) + } +} + +impl DavProperty { + pub fn is_all_prop(&self) -> bool { + matches!( + self, + DavProperty::WebDav(WebDavProperty::CreationDate) + | DavProperty::WebDav(WebDavProperty::DisplayName) + | DavProperty::WebDav(WebDavProperty::GetETag) + | DavProperty::WebDav(WebDavProperty::GetLastModified) + | DavProperty::WebDav(WebDavProperty::ResourceType) + | DavProperty::WebDav(WebDavProperty::LockDiscovery) + | DavProperty::WebDav(WebDavProperty::SupportedLock) + | DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal) + | DavProperty::WebDav(WebDavProperty::SyncToken) + | DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet) + | DavProperty::WebDav(WebDavProperty::AclRestrictions) + | DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet) + | DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet) + | DavProperty::WebDav(WebDavProperty::GetContentLanguage) + | DavProperty::WebDav(WebDavProperty::GetContentLength) + | DavProperty::WebDav(WebDavProperty::GetContentType) + | DavProperty::WebDav(WebDavProperty::SupportedReportSet) + | DavProperty::DeadProperty(_) + ) + } +} diff --git a/crates/dav-proto/src/schema/request.rs b/crates/dav-proto/src/schema/request.rs new file mode 100644 index 00000000..1ddeb12f --- /dev/null +++ b/crates/dav-proto/src/schema/request.rs @@ -0,0 +1,339 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use calcard::{ + icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, + vcard::{VCardParameterName, VCardProperty}, +}; + +use super::{ + property::{DateRange, DavProperty, DavValue, LockScope, LockType}, + response::Ace, + Collation, MatchType, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum PropFind { + PropName, + AllProp(Vec), + Prop(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct PropertyUpdate { + pub set: Vec, + pub remove: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct DavPropertyValue { + pub property: DavProperty, + pub value: DavValue, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct MkCol { + pub is_mkcalendar: bool, + pub props: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct LockInfo { + pub lock_scope: LockScope, + pub lock_type: LockType, + pub owner: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type"))] +pub enum Report { + Addressbook(AddressbookQuery), + AddressbookMultiGet(MultiGet), + CalendarQuery(CalendarQuery), + CalendarMultiGet(MultiGet), + FreeBusyQuery(FreeBusyQuery), + SyncCollection(SyncCollection), + AclPrincipalPropSet(AclPrincipalPropSet), + PrincipalMatch(PrincipalMatch), + PrincipalPropertySearch(PrincipalPropertySearch), + PrincipalSearchPropertySet, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct AddressbookQuery { + pub properties: PropFind, + pub filters: Vec>, + pub limit: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct VCardPropertyWithGroup { + pub name: VCardProperty, + pub group: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct CalendarQuery { + pub properties: PropFind, + pub filters: + Vec, ICalendarProperty, ICalendarParameterName>>, + pub timezone: Timezone, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type"))] +pub enum Timezone { + Name(String), + Id(String), + None, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct FreeBusyQuery { + pub range: DateRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct MultiGet { + pub properties: PropFind, + pub hrefs: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct SyncCollection { + pub sync_token: Option, + pub properties: PropFind, + pub level_inf: bool, + pub limit: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type"))] +pub enum Filter { + AnyOf, + AllOf, + Component { + comp: A, + op: FilterOp, + }, + Property { + comp: A, + prop: B, + op: FilterOp, + }, + Parameter { + comp: A, + prop: B, + param: C, + op: FilterOp, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +pub enum FilterOp { + Exists, + Undefined, + TimeRange(DateRange), + TextMatch(TextMatch), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type"))] +pub struct TextMatch { + pub match_type: MatchType, + pub value: String, + pub collation: Collation, + pub negate: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(tag = "type", content = "data"))] +#[rkyv(derive(Debug))] +pub enum DeadPropertyTag { + ElementStart(DeadElementTag), + ElementEnd, + Text(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[rkyv(derive(Debug))] +pub struct DeadElementTag { + pub name: String, + pub attrs: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(test, serde(transparent))] +#[rkyv(derive(Debug))] +#[repr(transparent)] +pub struct DeadProperty(pub Vec); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct Acl { + pub aces: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct AclPrincipalPropSet { + pub properties: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct PrincipalMatch { + pub principal_properties: PrincipalMatchProperties, + pub properties: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum PrincipalMatchProperties { + Properties(Vec), + Self_, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct PrincipalPropertySearch { + pub property_search: Vec, + pub properties: Vec, + pub apply_to_principal_collection_set: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct PropertySearch { + pub property: DavProperty, + pub match_: String, +} + +impl From<&ArchivedDeadProperty> for DeadProperty { + fn from(value: &ArchivedDeadProperty) -> Self { + DeadProperty(value.0.iter().map(|tag| tag.into()).collect::>()) + } +} + +impl From<&ArchivedDeadPropertyTag> for DeadPropertyTag { + fn from(tag: &ArchivedDeadPropertyTag) -> Self { + match tag { + ArchivedDeadPropertyTag::ElementStart(tag) => DeadPropertyTag::ElementStart(tag.into()), + ArchivedDeadPropertyTag::ElementEnd => DeadPropertyTag::ElementEnd, + ArchivedDeadPropertyTag::Text(tag) => DeadPropertyTag::Text(tag.to_string()), + } + } +} + +impl From<&ArchivedDeadElementTag> for DeadElementTag { + fn from(tag: &ArchivedDeadElementTag) -> Self { + DeadElementTag { + name: tag.name.to_string(), + attrs: tag.attrs.as_ref().map(|s| s.to_string()), + } + } +} + +impl ArchivedDeadProperty { + pub fn find_tag(&self, needle: &str) -> Option { + let mut depth: u32 = 0; + let mut tags = Vec::new(); + let mut found_tag = false; + + for tag in self.0.iter() { + match tag { + ArchivedDeadPropertyTag::ElementStart(start) => { + if depth == 0 && start.name == needle { + found_tag = true; + } else if found_tag { + tags.push(tag.into()); + } + + depth += 1; + } + ArchivedDeadPropertyTag::ElementEnd => { + if found_tag { + if depth == 1 { + break; + } else { + tags.push(tag.into()); + } + } + depth = depth.saturating_sub(1); + } + ArchivedDeadPropertyTag::Text(_) => { + if found_tag { + tags.push(tag.into()); + } + } + } + } + + if found_tag { + Some(DeadProperty(tags)) + } else { + None + } + } + + pub fn to_dav_values(&self, output: &mut Vec) { + let mut depth: u32 = 0; + let mut tags = Vec::new(); + let mut tag_start = None; + + for tag in self.0.iter() { + match tag { + ArchivedDeadPropertyTag::ElementStart(start) => { + if depth == 0 { + tag_start = Some(DeadElementTag::from(start)); + } else { + tags.push(tag.into()); + } + + depth += 1; + } + ArchivedDeadPropertyTag::ElementEnd => { + depth = depth.saturating_sub(1); + + if depth > 0 { + tags.push(tag.into()); + } else if let Some(tag_start) = tag_start.take() { + output.push(DavPropertyValue::new( + DavProperty::DeadProperty(tag_start), + DavValue::DeadProperty(DeadProperty(std::mem::take(&mut tags))), + )); + } + } + ArchivedDeadPropertyTag::Text(_) => { + if tag_start.is_some() { + tags.push(tag.into()); + } + } + } + } + } +} diff --git a/crates/dav-proto/src/schema/response.rs b/crates/dav-proto/src/schema/response.rs new file mode 100644 index 00000000..c8f4af5b --- /dev/null +++ b/crates/dav-proto/src/schema/response.rs @@ -0,0 +1,263 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::fmt::Display; + +use calcard::{ + icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, + vcard::{VCardParameterName, VCardProperty}, +}; +use hyper::StatusCode; + +use super::{ + property::{DavProperty, Privilege}, + request::{DavPropertyValue, Filter}, + Namespace, +}; + +pub struct MultiStatus { + pub namespace: Namespace, + pub response: List, + pub response_description: Option, + pub sync_token: Option, +} + +pub struct Response { + pub href: Href, + pub typ: ResponseType, + pub error: Option, + pub response_description: Option, + pub location: Option, +} + +pub enum ResponseType { + PropStat(List), + Status { href: List, status: Status }, +} + +#[repr(transparent)] +pub struct Status(pub StatusCode); + +#[repr(transparent)] +pub struct Location(pub Href); + +#[repr(transparent)] +pub struct ResponseDescription(pub String); + +#[repr(transparent)] +pub struct SyncToken(pub String); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[repr(transparent)] +pub struct Href(pub String); + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +#[repr(transparent)] +pub struct List(pub Vec); + +pub struct MkColResponse { + pub namespace: Namespace, + pub propstat: List, +} + +pub struct PropStat { + pub prop: Prop, + pub status: Status, + pub error: Option, + pub response_description: Option, +} + +#[repr(transparent)] +pub struct Prop(pub List); + +pub struct PropResponse { + pub namespace: Namespace, + pub properties: List, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct SupportedPrivilege { + pub privilege: Privilege, + pub abstract_: bool, + pub description: String, + pub supported_privilege: List, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct Ace { + pub principal: Principal, + pub invert: bool, + pub grant_deny: GrantDeny, + pub protected: bool, + pub inherited: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum GrantDeny { + Grant(List), + Deny(List), +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum Principal { + Href(Href), + All, + #[default] + Authenticated, + Unauthenticated, + Property(List), + Self_, +} + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct AclRestrictions { + pub grant_only: bool, + pub no_invert: bool, + pub deny_before_grant: bool, + pub required_principal: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub enum RequiredPrincipal { + All, + Authenticated, + Unauthenticated, + Self_, + Href(List), + Property(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct PrincipalSearchPropertySet { + pub namespace: Namespace, + pub properties: List, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] +pub struct PrincipalSearchProperty { + pub name: DavProperty, + pub description: String, +} + +pub struct ErrorResponse { + pub namespace: Namespace, + pub error: Condition, +} + +pub enum Condition { + Base(BaseCondition), + Cal(CalCondition), + Card(CardCondition), +} + +pub enum BaseCondition { + NoConflictingLock(List), + LockTokenSubmitted(List), + LockTokenMatchesRequestUri, + CannotModifyProtectedProperty, + NoExternalEntities, + PreservedLiveProperties, + PropFindFiniteDepth, + ResourceMustBeNull, + NeedPrivileges(List), + NoAceConflict, + NoProtectedAceConflict, + NoInheritedAceConflict, + LimitedNumberOfAces, + DenyBeforeGrant, + GrantOnly, + NoInvert, + NoAbstract, + NotSupportedPrivilege, + MissingRequiredPrincipal, + RecognizedPrincipal, + AllowedPrincipal, + NumberOfMatchesWithinLimit, + QuotaNotExceeded, + ValidResourceType, + ValidSyncToken, +} + +pub struct Resource { + pub href: Href, + pub privilege: Privilege, +} + +pub enum CalCondition { + CalendarCollectionLocationOk, + ValidCalendarData, + ValidFilter, + ValidCalendarObjectResource, + NoUidConflict(Href), + InitializeCalendarCollection, + SupportedCalendarData, + SupportedFilter( + Vec, ICalendarProperty, ICalendarParameterName>>, + ), + SupportedCollation(String), + MinDateTime, + MaxDateTime, + MaxResourceSize(u32), + MaxInstances, + MaxAttendeesPerInstance, +} + +pub enum CardCondition { + SupportedAddressData, + SupportedAddressDataConversion, + SupportedFilter(Vec>), + SupportedCollation(String), + ValidAddressData, + NoUidConflict(Href), + MaxResourceSize(u32), + AddressBoolCollectionLocationOk, +} + +impl BaseCondition { + pub fn status(&self) -> StatusCode { + match self { + BaseCondition::NoConflictingLock(_) => StatusCode::LOCKED, + BaseCondition::CannotModifyProtectedProperty => StatusCode::FORBIDDEN, + BaseCondition::LockTokenSubmitted(_) => StatusCode::LOCKED, + BaseCondition::LockTokenMatchesRequestUri => StatusCode::CONFLICT, + BaseCondition::NoExternalEntities => StatusCode::FORBIDDEN, + BaseCondition::PreservedLiveProperties => StatusCode::CONFLICT, + BaseCondition::PropFindFiniteDepth => StatusCode::FORBIDDEN, + BaseCondition::ResourceMustBeNull => StatusCode::CONFLICT, + BaseCondition::NeedPrivileges(_) => StatusCode::FORBIDDEN, + BaseCondition::NumberOfMatchesWithinLimit => StatusCode::FORBIDDEN, + _ => StatusCode::FORBIDDEN, + } + } +} + +impl From for Href { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for Href { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +impl MultiStatus { + pub fn is_empty(&self) -> bool { + self.response.0.is_empty() + } +} diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 7fdf574a..d8076158 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" resolver = "2" [dependencies] -dav-proto = { path = "/Users/me/code/dav-proto" } +dav-proto = { path = "../dav-proto" } common = { path = "../common" } store = { path = "../store" } utils = { path = "../utils" } diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index 8b57b459..d7fba69d 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -4,13 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ - use common::{Server, auth::AccessToken}; +use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; +use dav_proto::schema::{ + property::Privilege, + response::{Ace, GrantDeny, Href, Principal}, +}; +use directory::{QueryBy, backend::internal::PrincipalField}; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{acl::Acl, collection::Collection, value::ArchivedAclGrant}; +use rkyv::vec::ArchivedVec; +use store::ahash::AHashSet; use trc::AddContext; use utils::map::bitmap::Bitmap; -use crate::DavError; +use crate::{DavError, DavResource}; pub(crate) trait DavAclHandler: Sync + Send { fn validate_and_map_parent_acl( @@ -33,6 +40,11 @@ pub(crate) trait DavAclHandler: Sync + Send { child_acl: impl Into> + Send, parent_acl: impl Into> + Send, ) -> impl Future> + Send; + + fn resolve_ace( + &self, + unresolved_aces: Vec, + ) -> impl Future>> + Send; } impl DavAclHandler for Server { @@ -111,4 +123,125 @@ impl DavAclHandler for Server { Err(DavError::Code(StatusCode::FORBIDDEN)) } } + + async fn resolve_ace(&self, unresolved_aces: Vec) -> trc::Result> { + let mut aces = Vec::with_capacity(unresolved_aces.len()); + + for ace in unresolved_aces { + let grant_account_name = self + .directory() + .query(QueryBy::Id(ace.account_id), false) + .await + .caused_by(trc::location!())? + .and_then(|mut p| p.take_str(PrincipalField::Name)) + .unwrap_or_else(|| format!("_{}", ace.account_id)); + + aces.push(Ace::new( + Principal::Href(Href(format!( + "{}/{}", + DavResource::Principal.base_path(), + grant_account_name, + ))), + GrantDeny::grant(ace.privileges), + )); + } + + Ok(aces) + } +} + +pub(crate) struct UnresolvedAce { + account_id: u32, + privileges: Vec, +} + +pub(crate) trait Privileges { + fn ace(&self, account_id: u32, grants: &ArchivedVec) -> Vec; + + fn current_privilege_set( + &self, + account_id: u32, + grants: &ArchivedVec, + ) -> Vec; +} + +impl Privileges for AccessToken { + fn ace(&self, account_id: u32, grants: &ArchivedVec) -> Vec { + let mut aces = Vec::with_capacity(grants.len()); + if self.is_member(account_id) || grants.effective_acl(self).contains(Acl::Administer) { + for grant in grants.iter() { + let grant_account_id = u32::from(grant.account_id); + let mut privileges = Vec::with_capacity(4); + let acl = Bitmap::::from(&grant.grants); + if acl.contains(Acl::Read) || acl.contains(Acl::ReadItems) { + privileges.push(Privilege::Read); + } + if acl.contains(Acl::Modify) + || acl.contains(Acl::Delete) + || acl.contains(Acl::ModifyItems) + || acl.contains(Acl::RemoveItems) + { + privileges.push(Privilege::Write); + } + if acl.contains(Acl::Administer) { + privileges.push(Privilege::ReadAcl); + privileges.push(Privilege::WriteAcl); + } + if acl.contains(Acl::ReadFreeBusy) { + privileges.push(Privilege::ReadFreeBusy); + } + + aces.push(UnresolvedAce { + account_id: grant_account_id, + privileges, + }); + } + } + aces + } + + fn current_privilege_set( + &self, + account_id: u32, + grants: &ArchivedVec, + ) -> Vec { + 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, + ] + } else { + let mut acls = AHashSet::with_capacity(16); + for grant in grants.effective_acl(self) { + match grant { + Acl::Read | Acl::ReadItems => { + acls.insert(Privilege::Read); + acls.insert(Privilege::ReadCurrentUserPrivilegeSet); + } + Acl::Modify | Acl::Delete | Acl::ModifyItems | Acl::RemoveItems => { + acls.insert(Privilege::Write); + } + Acl::Administer => { + acls.insert(Privilege::ReadAcl); + acls.insert(Privilege::WriteAcl); + } + Acl::ReadFreeBusy => { + acls.insert(Privilege::ReadFreeBusy); + } + _ => {} + } + } + + acls.into_iter().collect() + } + } } diff --git a/crates/dav/src/common/mod.rs b/crates/dav/src/common/mod.rs index bed151cb..2610169f 100644 --- a/crates/dav/src/common/mod.rs +++ b/crates/dav/src/common/mod.rs @@ -113,4 +113,8 @@ impl<'x> DavQuery<'x> { 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 + } } diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 23128d07..fc1afb15 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -8,20 +8,22 @@ use common::{Server, auth::AccessToken}; use dav_proto::{ Depth, RequestHeaders, schema::{ - request::PropFind, - response::{BaseCondition, MultiStatus, Response}, + property::{DavProperty, ResourceType, WebDavProperty}, + request::{DavPropertyValue, PropFind}, + response::{BaseCondition, MultiStatus, PropStat, Response}, }, }; use http_proto::HttpResponse; use hyper::StatusCode; use jmap_proto::types::collection::Collection; use store::roaring::RoaringBitmap; +use trc::AddContext; use crate::{ DavErrorCondition, common::uri::DavUriResource, file::propfind::HandleFilePropFindRequest, - principal::propfind::{PrincipalPropFind, PrincipalResource}, + principal::{CurrentUserPrincipal, propfind::PrincipalPropFind}, }; use super::{DavQuery, uri::UriResource}; @@ -33,6 +35,12 @@ pub(crate) trait PropFindRequestHandler: Sync + Send { headers: RequestHeaders<'_>, request: PropFind, ) -> impl Future> + Send; + + fn dav_quota( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> impl Future> + Send; } impl PropFindRequestHandler for Server { @@ -94,7 +102,8 @@ impl PropFindRequestHandler for Server { } else { self.prepare_principal_propfind_response( access_token, - PrincipalResource::Id(account_id), + Collection::Principal, + [account_id].into_iter(), &request, &mut response, ) @@ -111,7 +120,40 @@ impl PropFindRequestHandler for Server { // Add container info if !headers.depth_no_root { - let blah = 1; + 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() + } + }; + + if !matches!(request, PropFind::PropName) { + for prop in &mut prop_stat { + match &prop.property { + DavProperty::WebDav(WebDavProperty::ResourceType) => { + prop.value = vec![ResourceType::Collection].into(); + } + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal) => { + prop.value = vec![access_token.current_user_principal()].into(); + } + _ => (), + } + } + } + + response.add_response(Response::new_propstat( + resource.base_path(), + vec![PropStat::new_list(prop_stat)], + )); } if return_children { @@ -126,7 +168,8 @@ impl PropFindRequestHandler for Server { self.prepare_principal_propfind_response( access_token, - PrincipalResource::Ids(ids), + resource.collection, + ids.into_iter(), &request, &mut response, ) @@ -136,4 +179,29 @@ impl PropFindRequestHandler for Server { Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) } } + + async fn dav_quota( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> trc::Result<(u64, u64)> { + let resource_token = self + .get_resource_token(access_token, account_id) + .await + .caused_by(trc::location!())?; + let quota = if resource_token.quota > 0 { + resource_token.quota + } else if let Some(tenant) = resource_token.tenant.filter(|t| t.quota > 0) { + tenant.quota + } else { + u64::MAX + }; + let quota_used = self + .get_used_quota(account_id) + .await + .caused_by(trc::location!())? as u64; + let quota_available = quota.saturating_sub(quota_used); + + Ok((quota_used, quota_available)) + } } diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index 4bb41e39..9c3246f8 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -123,6 +123,12 @@ impl OwnedUri<'_> { } } +impl UriResource { + pub fn base_path(&self) -> &'static str { + DavResource::from(self.collection).base_path() + } +} + impl Urn { pub fn parse(input: &str) -> Option { let inbox = input.strip_prefix("urn:stalwart:")?; diff --git a/crates/dav/src/file/acl.rs b/crates/dav/src/file/acl.rs index ccc2e77e..5885f323 100644 --- a/crates/dav/src/file/acl.rs +++ b/crates/dav/src/file/acl.rs @@ -6,13 +6,14 @@ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; -use groupware::file::ArchivedFileNode; +use groupware::file::{ArchivedFileNode, FileNode, hierarchy::FileHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; -use jmap_proto::types::{acl::Acl, collection::Collection}; +use jmap_proto::types::{acl::Acl, collection::Collection, property::Property}; +use store::write::{AlignedBytes, Archive}; use trc::AddContext; -use crate::DavError; +use crate::{DavError, common::uri::DavUriResource, file::DavFileResource}; pub(crate) trait FileAclRequestHandler: Sync + Send { fn handle_file_acl_request( @@ -39,6 +40,43 @@ impl FileAclRequestHandler for Server { headers: RequestHeaders<'_>, request: dav_proto::schema::request::Acl, ) -> crate::Result { + // Validate URI + let resource_ = self + .validate_uri(access_token, headers.uri) + .await? + .into_owned_uri()?; + let account_id = resource_.account_id; + let files = self + .fetch_file_hierarchy(account_id) + .await + .caused_by(trc::location!())?; + let resource = files.map_resource(&resource_)?; + + // Fetch node + let node_ = self + .get_property::>( + account_id, + Collection::FileNode, + resource.resource, + Property::Value, + ) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; + let node = node_.unarchive::().caused_by(trc::location!())?; + + // Validate ACL + self.validate_file_acl( + access_token, + account_id, + node, + Acl::Administer, + Acl::Administer, + ) + .await?; + + for ace in request.aces {} + todo!() } diff --git a/crates/dav/src/file/mkcol.rs b/crates/dav/src/file/mkcol.rs index 245a54c4..c256774c 100644 --- a/crates/dav/src/file/mkcol.rs +++ b/crates/dav/src/file/mkcol.rs @@ -6,7 +6,7 @@ use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use dav_proto::{ - RequestHeaders, + RequestHeaders, Return, schema::{Namespace, request::MkCol, response::MkColResponse}, }; use groupware::file::{FileNode, hierarchy::FileHierarchy}; @@ -98,6 +98,7 @@ impl FileMkColRequestHandler for Server { }; // Apply MKCOL properties + let mut return_prop_stat = None; if let Some(mkcol) = request { let mut prop_stat = Vec::new(); if !self.apply_file_properties(&mut node, false, mkcol.props, &mut prop_stat) { @@ -107,6 +108,9 @@ impl FileMkColRequestHandler for Server { .to_string(), )); } + if headers.ret != Return::Minimal { + return_prop_stat = Some(prop_stat); + } } // Prepare write batch @@ -127,7 +131,14 @@ impl FileMkColRequestHandler for Server { // Broadcast state change self.broadcast_single_state_change(account_id, change_id, DataType::FileNode) .await; - - Ok(HttpResponse::new(StatusCode::CREATED)) + if let Some(prop_stat) = return_prop_stat { + Ok(HttpResponse::new(StatusCode::CREATED).with_xml_body( + MkColResponse::new(prop_stat) + .with_namespace(Namespace::Dav) + .to_string(), + )) + } else { + Ok(HttpResponse::new(StatusCode::CREATED)) + } } } diff --git a/crates/dav/src/file/propfind.rs b/crates/dav/src/file/propfind.rs index 49cb1a53..8efb5fd5 100644 --- a/crates/dav/src/file/propfind.rs +++ b/crates/dav/src/file/propfind.rs @@ -7,11 +7,13 @@ use common::{FileItem, Server, auth::AccessToken}; use dav_proto::schema::{ property::{ - DavProperty, DavValue, ReportSet, ResourceType, Rfc1123DateTime, SupportedLock, + DavProperty, DavValue, Privilege, ReportSet, ResourceType, Rfc1123DateTime, SupportedLock, WebDavProperty, }, request::{DavPropertyValue, PropFind}, - response::{MultiStatus, PropStat, Response}, + response::{ + AclRestrictions, Href, MultiStatus, PropStat, Response, ResponseType, SupportedPrivilege, + }, }; use groupware::file::{FileNode, hierarchy::FileHierarchy}; use http_proto::HttpResponse; @@ -28,8 +30,15 @@ use trc::AddContext; use utils::map::bitmap::Bitmap; use crate::{ - common::{DavQuery, ETag, lock::LockData, uri::Urn}, - principal::propfind::{PrincipalPropFind, PrincipalResource}, + DavResource, + common::{ + DavQuery, ETag, + acl::{DavAclHandler, Privileges}, + lock::LockData, + propfind::PropFindRequestHandler, + uri::Urn, + }, + principal::{CurrentUserPrincipal, propfind::PrincipalPropFind}, }; pub(crate) trait HandleFilePropFindRequest: Sync + Send { @@ -54,7 +63,6 @@ impl HandleFilePropFindRequest for Server { // Obtain document ids let mut document_ids = if !access_token.is_member(account_id) { - let todo = "query children acls"; self.shared_containers( access_token, account_id, @@ -115,7 +123,8 @@ impl HandleFilePropFindRequest for Server { if !query.depth_no_root || query.from_change_id.is_none() { self.prepare_principal_propfind_response( access_token, - PrincipalResource::Id(account_id), + Collection::FileNode, + [account_id].into_iter(), &query.propfind, &mut response, ) @@ -144,8 +153,6 @@ impl HandleFilePropFindRequest for Server { ); } - let todo = "prefer minimal"; - // Prepare response let (fields, is_all_prop) = match &query.propfind { PropFind::PropName => { @@ -226,6 +233,36 @@ impl HandleFilePropFindRequest for Server { } } + // Fetch quota + let (quota_used, quota_available) = if fields.iter().any(|field| { + matches!( + field, + DavProperty::WebDav( + WebDavProperty::QuotaAvailableBytes | WebDavProperty::QuotaUsedBytes + ) + ) + }) { + self.dav_quota(access_token, account_id) + .await + .caused_by(trc::location!())? + } else { + (0, 0) + }; + + // Fetch owner + let mut owner = None; + if fields + .iter() + .any(|field| matches!(field, DavProperty::WebDav(WebDavProperty::Owner))) + { + owner = self + .owner_href(access_token, account_id) + .await + .caused_by(trc::location!())? + .into(); + } + + let mut aces = Vec::new(); self.get_archives( account_id, Collection::FileNode, @@ -234,13 +271,12 @@ impl HandleFilePropFindRequest for Server { |document_id, node_| { let node = node_.unarchive::().caused_by(trc::location!())?; let item = paths.items.get(&document_id).unwrap(); - let is_container = node.file.is_none(); let properties: Box> = if is_all_prop { - Box::new(if is_container { - FOLDER_PROPS.iter() - } else { - FILE_PROPS.iter() - }) + Box::new( + ALL_PROPS + .iter() + .chain(fields.iter().filter(|field| !field.is_all_prop())), + ) } else { Box::new(fields.iter()) }; @@ -357,26 +393,124 @@ impl HandleFilePropFindRequest for Server { sync_token.clone().unwrap(), )); } - WebDavProperty::CurrentUserPrincipal => todo!(), - WebDavProperty::QuotaAvailableBytes => todo!(), - WebDavProperty::QuotaUsedBytes => todo!(), - WebDavProperty::AlternateURISet => todo!(), - WebDavProperty::PrincipalURL => todo!(), - WebDavProperty::GroupMemberSet => todo!(), - WebDavProperty::GroupMembership => todo!(), - WebDavProperty::Owner => todo!(), - WebDavProperty::Group => { - if !is_all_prop { + WebDavProperty::CurrentUserPrincipal => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![access_token.current_user_principal()], + )); + } + WebDavProperty::QuotaAvailableBytes => { + if node.file.is_none() { + fields.push(DavPropertyValue::new( + property.clone(), + quota_available, + )); + } else if !is_all_prop { fields_not_found .push(DavPropertyValue::empty(property.clone())); } } - WebDavProperty::SupportedPrivilegeSet => todo!(), - WebDavProperty::CurrentUserPrivilegeSet => todo!(), - WebDavProperty::Acl => todo!(), - WebDavProperty::AclRestrictions => todo!(), - WebDavProperty::InheritedAclSet => todo!(), - WebDavProperty::PrincipalCollectionSet => todo!(), + WebDavProperty::QuotaUsedBytes => { + if node.file.is_none() { + fields + .push(DavPropertyValue::new(property.clone(), quota_used)); + } else if !is_all_prop { + fields_not_found + .push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::Owner => { + if let Some(owner) = owner.take() { + fields + .push(DavPropertyValue::new(property.clone(), vec![owner])); + } + } + WebDavProperty::Group => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::SupportedPrivilegeSet => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ + SupportedPrivilege::new(Privilege::All, "Any operation") + .with_abstract() + .with_supported_privilege( + SupportedPrivilege::new( + Privilege::Read, + "Read objects", + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::ReadCurrentUserPrivilegeSet, + "Read current user privileges", + )), + ) + .with_supported_privilege( + SupportedPrivilege::new( + Privilege::Write, + "Write objects", + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteProperties, + "Write properties", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteContent, + "Write object contents", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Bind, + "Add resources to a collection", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Unbind, + "Add resources to a collection", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::Unlock, + "Unlock resources", + )), + ) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::ReadAcl, + "Read ACL", + )) + .with_supported_privilege(SupportedPrivilege::new( + Privilege::WriteAcl, + "Write ACL", + )), + ], + )); + } + WebDavProperty::CurrentUserPrivilegeSet => { + fields.push(DavPropertyValue::new( + property.clone(), + access_token.current_privilege_set(account_id, &node.acls), + )); + } + WebDavProperty::Acl => { + aces.push(access_token.ace(account_id, &node.acls)); + } + WebDavProperty::AclRestrictions => { + fields.push(DavPropertyValue::new( + property.clone(), + AclRestrictions::default().with_no_invert(), + )); + } + WebDavProperty::InheritedAclSet => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::PrincipalCollectionSet => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![Href(DavResource::Principal.base_path().to_string())], + )); + } + WebDavProperty::AlternateURISet + | WebDavProperty::PrincipalURL + | WebDavProperty::GroupMemberSet + | WebDavProperty::GroupMembership => { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } }, DavProperty::DeadProperty(tag) => { if let Some(value) = node.dead_properties.find_tag(&tag.name) { @@ -400,14 +534,17 @@ impl HandleFilePropFindRequest for Server { // Add response let mut prop_stat = Vec::with_capacity(2); - if !fields.is_empty() { + if !fields.is_empty() || !aces.is_empty() { prop_stat.push(PropStat::new_list(fields)); } - if !fields_not_found.is_empty() { + if !fields_not_found.is_empty() && !query.is_minimal() { prop_stat.push( PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND), ); } + if prop_stat.is_empty() { + prop_stat.push(PropStat::new_list(vec![])); + } response.add_response(Response::new_propstat( query.format_to_base_uri(&item.name), prop_stat, @@ -419,6 +556,25 @@ impl HandleFilePropFindRequest for Server { .await .caused_by(trc::location!())?; + // Resolve ACEs + if !aces.is_empty() { + for (ace, response) in aces.into_iter().zip(response.response.0.iter_mut()) { + let ace = self.resolve_ace(ace).await.caused_by(trc::location!())?; + if let ResponseType::PropStat(list) = &mut response.typ { + list.0 + .first_mut() + .unwrap() + .prop + .0 + .0 + .push(DavPropertyValue::new( + DavProperty::WebDav(WebDavProperty::Acl), + ace, + )); + } + } + } + Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string())) } } @@ -467,6 +623,26 @@ static FILE_PROPS: [DavProperty; 19] = [ DavProperty::WebDav(WebDavProperty::GetContentType), ]; +static ALL_PROPS: [DavProperty; 17] = [ + DavProperty::WebDav(WebDavProperty::CreationDate), + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::GetETag), + DavProperty::WebDav(WebDavProperty::GetLastModified), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::LockDiscovery), + DavProperty::WebDav(WebDavProperty::SupportedLock), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::SyncToken), + DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet), + DavProperty::WebDav(WebDavProperty::AclRestrictions), + DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + DavProperty::WebDav(WebDavProperty::GetContentLanguage), + DavProperty::WebDav(WebDavProperty::GetContentLength), + DavProperty::WebDav(WebDavProperty::GetContentType), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), +]; + struct Paths<'x> { min: u32, max: u32, diff --git a/crates/dav/src/file/proppatch.rs b/crates/dav/src/file/proppatch.rs index 24bab344..32a66ef8 100644 --- a/crates/dav/src/file/proppatch.rs +++ b/crates/dav/src/file/proppatch.rs @@ -6,7 +6,7 @@ use common::{Server, auth::AccessToken}; use dav_proto::{ - RequestHeaders, + RequestHeaders, Return, schema::{ property::{DavProperty, DavValue, ResourceType, WebDavProperty}, request::{DavPropertyValue, PropertyUpdate}, @@ -150,7 +150,7 @@ impl FilePropPatchRequestHandler for Server { } // Set properties - self.apply_file_properties(&mut new_node, true, request.set, &mut items); + let is_success = self.apply_file_properties(&mut new_node, true, request.set, &mut items); let etag = if new_node != node.inner { update_file_node( @@ -168,9 +168,15 @@ impl FilePropPatchRequestHandler for Server { node_.etag().into() }; - Ok(HttpResponse::new(StatusCode::MULTI_STATUS) - .with_xml_body(MultiStatus::new(vec![Response::new_propstat(uri, items)]).to_string()) - .with_etag_opt(etag)) + if headers.ret != Return::Minimal || !is_success { + Ok(HttpResponse::new(StatusCode::MULTI_STATUS) + .with_xml_body( + MultiStatus::new(vec![Response::new_propstat(uri, items)]).to_string(), + ) + .with_etag_opt(etag)) + } else { + Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) + } } fn apply_file_properties( diff --git a/crates/dav/src/file/update.rs b/crates/dav/src/file/update.rs index 51ab50bb..f482dd16 100644 --- a/crates/dav/src/file/update.rs +++ b/crates/dav/src/file/update.rs @@ -5,7 +5,7 @@ */ use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; -use dav_proto::RequestHeaders; +use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime}; use groupware::file::{FileNode, FileProperties, hierarchy::FileHierarchy}; use http_proto::HttpResponse; use hyper::StatusCode; @@ -92,23 +92,6 @@ impl FileUpdateRequestHandler for Server { ) .await?; - // Validate headers - self.validate_headers( - access_token, - &headers, - vec![ResourceState { - account_id, - collection: resource.collection, - document_id: Some(document_id), - etag: node_archive_.etag().into(), - path: resource_name, - ..Default::default() - }], - Default::default(), - DavMethod::PUT, - ) - .await?; - // Verify that the node is a file if let Some(file) = node.file.as_ref() { if BlobHash::generate(&bytes).as_slice() == file.blob_hash.0.as_slice() { @@ -118,6 +101,53 @@ impl FileUpdateRequestHandler for Server { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); } + // Validate headers + match self + .validate_headers( + access_token, + &headers, + vec![ResourceState { + account_id, + collection: resource.collection, + document_id: Some(document_id), + etag: node_archive_.etag().into(), + path: resource_name, + ..Default::default() + }], + Default::default(), + DavMethod::PUT, + ) + .await + { + Ok(_) => {} + Err(DavError::Code(StatusCode::PRECONDITION_FAILED)) + if headers.ret == Return::Representation => + { + let file = node.file.as_ref().unwrap(); + let contents = self + .blob_store() + .get_blob(file.blob_hash.0.as_slice(), 0..usize::MAX) + .await + .caused_by(trc::location!())? + .ok_or(DavError::Code(StatusCode::PRECONDITION_FAILED))?; + + return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED) + .with_content_type( + file.media_type + .as_ref() + .map(|v| v.as_str()) + .unwrap_or("application/octet-stream"), + ) + .with_etag(node_archive_.etag()) + .with_last_modified( + Rfc1123DateTime::new(i64::from(node.modified)).to_string(), + ) + .with_header("Preference-Applied", "return=representation") + .with_binary_body(contents)); + } + Err(e) => return Err(e), + } + // Validate quota let extra_bytes = (bytes.len() as u64) .saturating_sub(u32::from(node.file.as_ref().unwrap().size) as u64); diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 310e568d..cac5b490 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -18,7 +18,7 @@ use jmap_proto::types::collection::Collection; pub(crate) type Result = std::result::Result; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DavResource { Card, Cal, @@ -93,6 +93,18 @@ impl From for Collection { } } +impl From 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 { hashify::tiny_map!(service.as_bytes(), @@ -103,10 +115,50 @@ impl DavResource { ) } - pub fn into_options_response(self) -> HttpResponse { - let todo = "true"; + 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", "1, 2, 3, access-control, calendar-access") + .with_header("DAV", dav) + .with_header("Allow", allow) } } diff --git a/crates/dav/src/principal/mod.rs b/crates/dav/src/principal/mod.rs index ba163334..d961d6f2 100644 --- a/crates/dav/src/principal/mod.rs +++ b/crates/dav/src/principal/mod.rs @@ -4,4 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::auth::AccessToken; +use dav_proto::schema::response::Href; +use percent_encoding::NON_ALPHANUMERIC; + +use crate::DavResource; + pub mod propfind; + +pub trait CurrentUserPrincipal { + fn current_user_principal(&self) -> Href; +} + +impl CurrentUserPrincipal for AccessToken { + fn current_user_principal(&self) -> Href { + Href(format!( + "{}/{}", + DavResource::Principal.base_path(), + percent_encoding::utf8_percent_encode(&self.name, NON_ALPHANUMERIC) + )) + } +} diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index 3402492b..fe6dfa5c 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -4,38 +4,290 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; -use dav_proto::{ - RequestHeaders, - schema::{request::PropFind, response::MultiStatus}, -}; -use http_proto::HttpResponse; -use store::roaring::RoaringBitmap; +use std::borrow::Cow; -pub(crate) enum PrincipalResource<'x> { - Id(u32), - Uri(&'x str), - Ids(RoaringBitmap), -} +use common::{Server, auth::AccessToken}; +use dav_proto::schema::{ + property::{DavProperty, ReportSet, ResourceType, WebDavProperty}, + request::{DavPropertyValue, PropFind}, + response::{Href, MultiStatus, PropStat, Response}, +}; +use directory::{QueryBy, backend::internal::PrincipalField}; +use hyper::StatusCode; +use jmap_proto::types::collection::Collection; +use percent_encoding::NON_ALPHANUMERIC; +use trc::AddContext; + +use crate::{ + DavResource, + common::{propfind::PropFindRequestHandler, uri::Urn}, +}; + +use super::CurrentUserPrincipal; pub(crate) trait PrincipalPropFind: Sync + Send { fn prepare_principal_propfind_response( &self, access_token: &AccessToken, - resource: PrincipalResource<'_>, + collection: Collection, + documents: impl Iterator + Sync + Send, request: &PropFind, response: &mut MultiStatus, - ) -> impl Future> + Send; + ) -> impl Future> + Send; + + fn owner_href( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> impl Future> + Send; } impl PrincipalPropFind for Server { async fn prepare_principal_propfind_response( &self, access_token: &AccessToken, - resource: PrincipalResource<'_>, + collection: Collection, + account_ids: impl Iterator + Sync + Send, request: &PropFind, response: &mut MultiStatus, - ) -> crate::Result { - todo!() + ) -> crate::Result<()> { + let properties = match request { + PropFind::PropName => { + let props = all_props(collection, None); + for account_id in account_ids { + response.add_response(Response::new_propstat( + self.owner_href(access_token, account_id) + .await + .caused_by(trc::location!())?, + vec![PropStat::new_list( + props.iter().cloned().map(DavPropertyValue::empty).collect(), + )], + )); + } + return Ok(()); + } + PropFind::AllProp(items) => Cow::Owned(all_props(collection, items.as_slice().into())), + PropFind::Prop(items) => Cow::Borrowed(items), + }; + let is_principal = collection == Collection::Principal; + let base_path = DavResource::from(collection).base_path(); + let needs_quota = properties.iter().any(|property| { + matches!( + property, + DavProperty::WebDav( + WebDavProperty::QuotaAvailableBytes | WebDavProperty::QuotaUsedBytes + ) + ) + }); + + for account_id in account_ids { + let mut fields = Vec::with_capacity(properties.len()); + let mut fields_not_found = Vec::new(); + + let (name, description) = if access_token.primary_id() == account_id { + ( + Cow::Borrowed(access_token.name.as_str()), + access_token + .description + .as_deref() + .unwrap_or(&access_token.name) + .to_string(), + ) + } else { + self.directory() + .query(QueryBy::Id(account_id), false) + .await + .caused_by(trc::location!())? + .map(|mut p| { + let name = p + .take_str(PrincipalField::Name) + .unwrap_or_else(|| format!("_{account_id}")); + let description = p + .take_str(PrincipalField::Description) + .unwrap_or_else(|| name.clone()); + (Cow::Owned(name), description) + }) + .unwrap_or_else(|| { + ( + Cow::Owned(format!("_{}", account_id)), + format!("_{}", account_id), + ) + }) + }; + + // Fetch quota + let (quota_used, quota_available) = if needs_quota { + self.dav_quota(access_token, account_id) + .await + .caused_by(trc::location!())? + } else { + (0, 0) + }; + + for property in properties.as_slice() { + match property { + DavProperty::WebDav(dav_property) => match dav_property { + WebDavProperty::DisplayName => { + fields + .push(DavPropertyValue::new(property.clone(), description.clone())); + } + WebDavProperty::ResourceType => { + if !is_principal { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ResourceType::Collection], + )); + } else { + fields.push(DavPropertyValue::empty(property.clone())); + } + } + WebDavProperty::SupportedReportSet if !is_principal => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![ReportSet::SyncCollection], + )); + } + WebDavProperty::CurrentUserPrincipal => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![access_token.current_user_principal()], + )); + } + WebDavProperty::QuotaAvailableBytes if !is_principal => { + fields.push(DavPropertyValue::new(property.clone(), quota_available)); + } + WebDavProperty::QuotaUsedBytes if !is_principal => { + fields.push(DavPropertyValue::new(property.clone(), quota_used)); + } + WebDavProperty::SyncToken if !is_principal => { + let id = self + .store() + .get_last_change_id(account_id, collection) + .await + .caused_by(trc::location!())? + .unwrap_or_default(); + fields.push(DavPropertyValue::new( + property.clone(), + Urn::Sync { id }.to_string(), + )); + } + WebDavProperty::AlternateURISet if is_principal => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::GroupMemberSet if is_principal => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::GroupMembership if is_principal => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::Owner | WebDavProperty::PrincipalURL => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![Href(format!( + "{}/{}", + DavResource::Principal.base_path(), + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ))], + )); + } + WebDavProperty::Group if !is_principal => { + fields.push(DavPropertyValue::empty(property.clone())); + } + WebDavProperty::PrincipalCollectionSet => { + fields.push(DavPropertyValue::new( + property.clone(), + vec![Href(DavResource::Principal.base_path().to_string())], + )); + } + _ => { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + }, + _ => { + fields_not_found.push(DavPropertyValue::empty(property.clone())); + } + } + } + + let mut prop_stats = Vec::with_capacity(2); + + if !fields.is_empty() { + prop_stats.push(PropStat::new_list(fields)); + } + + if !fields_not_found.is_empty() { + prop_stats + .push(PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND)); + } + + response.add_response(Response::new_propstat( + Href(format!( + "{}/{}", + base_path, + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + )), + prop_stats, + )); + } + + Ok(()) + } + + async fn owner_href(&self, access_token: &AccessToken, account_id: u32) -> trc::Result { + if access_token.primary_id() == account_id { + Ok(access_token.current_user_principal()) + } else { + let name = self + .directory() + .query(QueryBy::Id(account_id), false) + .await + .caused_by(trc::location!())? + .and_then(|mut p| p.take_str(PrincipalField::Name)) + .unwrap_or_else(|| format!("_{account_id}")); + Ok(Href(format!( + "{}/{}", + DavResource::Principal.base_path(), + percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC), + ))) + } + } +} + +fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec { + if collection == Collection::Principal { + vec![ + DavProperty::WebDav(WebDavProperty::DisplayName), + DavProperty::WebDav(WebDavProperty::ResourceType), + DavProperty::WebDav(WebDavProperty::SupportedReportSet), + DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal), + DavProperty::WebDav(WebDavProperty::AlternateURISet), + DavProperty::WebDav(WebDavProperty::PrincipalURL), + DavProperty::WebDav(WebDavProperty::GroupMemberSet), + DavProperty::WebDav(WebDavProperty::GroupMembership), + DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet), + ] + } else if let Some(all_props) = all_props { + let mut props = 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), + ]; + + 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), + ] } } diff --git a/crates/dav/src/request.rs b/crates/dav/src/request.rs index b40c3f00..9d0cc1f1 100644 --- a/crates/dav/src/request.rs +++ b/crates/dav/src/request.rs @@ -71,8 +71,6 @@ impl DavRequestDispatcher for Server { } // Dispatch - let todo = "lock tokens, headers, etc"; - match method { DavMethod::PROPFIND => { self.handle_propfind_request( @@ -189,14 +187,19 @@ impl DavRequestDispatcher for Server { } }, DavMethod::UNLOCK => self.handle_lock_request(&access_token, headers, None).await, - DavMethod::ACL => { - self.handle_file_acl_request( - &access_token, - headers, - Acl::parse(&mut Tokenizer::new(&body))?, - ) - .await - } + DavMethod::ACL => match resource { + DavResource::Card => todo!(), + DavResource::Cal => todo!(), + DavResource::File => { + self.handle_file_acl_request( + &access_token, + headers, + Acl::parse(&mut Tokenizer::new(&body))?, + ) + .await + } + DavResource::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)), + }, DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? { Report::SyncCollection(sync_collection) => { let uri = self @@ -219,7 +222,7 @@ impl DavRequestDispatcher for Server { } } Report::Addressbook(addressbook_query) => todo!(), - Report::AdressbookMultiGet(multi_get) => todo!(), + Report::AddressbookMultiGet(multi_get) => todo!(), Report::CalendarQuery(calendar_query) => todo!(), Report::CalendarMultiGet(multi_get) => todo!(), Report::FreeBusyQuery(free_busy_query) => todo!(), diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index 2bc6a7dc..eaaa33d2 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -11,7 +11,7 @@ common = { path = "../common" } jmap_proto = { path = "../jmap-proto" } trc = { path = "../trc" } directory = { path = "../directory" } -dav-proto = { path = "/Users/me/code/dav-proto" } +dav-proto = { path = "../dav-proto" } calcard = { path = "/Users/me/code/calcard" } hashify = "0.2" rkyv = { version = "0.8.10", features = ["little_endian"] } diff --git a/crates/http-proto/src/response.rs b/crates/http-proto/src/response.rs index c71d193b..15b75120 100644 --- a/crates/http-proto/src/response.rs +++ b/crates/http-proto/src/response.rs @@ -171,7 +171,7 @@ impl HttpResponse { .map_err(|never| match never {}) .boxed(), ), - HttpResponseBody::Empty => self.builder.body( + HttpResponseBody::Empty => self.builder.header(header::CONTENT_LENGTH, 0).body( Full::new(Bytes::new()) .map_err(|never| match never {}) .boxed(), diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 6f0538e7..54dbcd93 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -209,7 +209,9 @@ impl ParseHttp for Server { path.next().and_then(DavResource::parse), DavMethod::parse(req.method()), ) { - (Some(resource), Some(DavMethod::OPTIONS)) => resource.into_options_response(), + (Some(resource), Some(DavMethod::OPTIONS)) => { + resource.into_options_response(path.count()) + } (Some(resource), Some(method)) => { // Authenticate request let (_in_flight, access_token) =