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