Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::vcard::VCardVersion;
use compact_str::{CompactString, ToCompactString};
use trc::Value;
pub mod parser;
pub mod requests;
pub mod responses;
pub mod schema;
pub fn xml_pretty_print(xml_string: &str) -> String {
// Create a reader
let mut reader = quick_xml::Reader::from_str(xml_string);
let mut writer = quick_xml::Writer::new_with_indent(std::io::Cursor::new(Vec::new()), b' ', 2);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(quick_xml::events::Event::Eof) => break,
Ok(event) => {
writer.write_event(event).unwrap();
}
Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
}
buf.clear();
}
let result = writer.into_inner().into_inner();
String::from_utf8(result).unwrap()
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct RequestHeaders<'x> {
pub uri: &'x str,
pub depth: Depth,
pub timeout: Timeout,
pub content_type: Option<&'x str>,
pub destination: Option<&'x str>,
pub lock_token: Option<&'x str>,
pub vcard_version: Option<VCardVersion>,
pub no_schedule_reply: bool,
pub if_schedule_tag: Option<u32>,
pub overwrite_fail: bool,
pub no_timezones: bool,
pub ret: Return,
pub depth_no_root: bool,
pub if_: Vec<If<'x>>,
pub range: Option<ByteRange>,
pub if_range: Option<&'x str>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ByteRange {
Offset { start: u64, end: Option<u64> },
Suffix(u64),
}
pub struct ResourceState<T: AsRef<str>> {
pub resource: Option<T>,
pub etag: T,
pub state_token: T,
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum Return {
Minimal,
Representation,
#[default]
Default,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct If<'x> {
pub resource: Option<&'x str>,
pub list: Vec<Condition<'x>>,
}
#[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,
}
impl From<&RequestHeaders<'_>> for Value {
fn from(headers: &RequestHeaders<'_>) -> Self {
let mut values = Vec::with_capacity(4);
if headers.depth != Depth::None {
values.push(Value::String(CompactString::const_new("Depth")));
values.push(match headers.depth {
Depth::Zero => Value::Int(0),
Depth::One => Value::Int(1),
Depth::Infinity => Value::String(CompactString::const_new("infinity")),
Depth::None => Value::None,
});
}
if headers.timeout != Timeout::None {
values.push(Value::String(CompactString::const_new("Timeout")));
values.push(match headers.timeout {
Timeout::Infinite => Value::String(CompactString::const_new("infinite")),
Timeout::Second(n) => Value::Int(n as i64),
Timeout::None => Value::None,
});
}
for (name, header_value) in [
("Content-Type", headers.content_type),
("Destination", headers.destination),
("Lock-Token", headers.lock_token),
("If-Range", headers.if_range),
] {
if let Some(value) = header_value {
values.push(CompactString::const_new(name).into());
values.push(value.to_compact_string().into());
}
}
if let Some(range) = headers.range {
values.push(CompactString::const_new("Range").into());
values.push(
match range {
ByteRange::Offset {
start,
end: Some(end),
} => format!("{start}-{end}"),
ByteRange::Offset { start, end: None } => format!("{start}-"),
ByteRange::Suffix(length) => format!("-{length}"),
}
.to_compact_string()
.into(),
);
}
for (name, is_set) in [
("Overwrite", headers.overwrite_fail),
("No-Timezones", headers.no_timezones),
("Depth-No-Root", headers.depth_no_root),
] {
if is_set {
values.push(CompactString::const_new(name).into());
}
}
for if_ in &headers.if_ {
values.push(CompactString::const_new("If").into());
let mut if_values = Vec::with_capacity(if_.list.len() * 2 + 1);
if let Some(resource) = if_.resource {
if_values.push(Value::String(resource.to_compact_string()));
}
for condition in &if_.list {
match condition {
Condition::StateToken { is_not, token } => {
if *is_not {
if_values.push(Value::String(CompactString::const_new("!State-Token")));
} else {
if_values.push(Value::String(CompactString::const_new("State-Token")));
}
if_values.push(Value::String(token.to_compact_string()));
}
Condition::ETag { is_not, tag } => {
if *is_not {
if_values.push(Value::String(CompactString::const_new("!ETag")));
} else {
if_values.push(Value::String(CompactString::const_new("ETag")));
}
if_values.push(Value::String(tag.to_compact_string()));
}
Condition::Exists { is_not } => {
if *is_not {
if_values.push(Value::String(CompactString::const_new("!Exists")));
} else {
if_values.push(Value::String(CompactString::const_new("Exists")));
}
}
}
}
values.push(Value::Array(if_values));
}
Value::Array(values)
}
}
/*
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
*/
+950
View File
@@ -0,0 +1,950 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{ByteRange, Condition, Depth, If, RequestHeaders, ResourceState, Return, Timeout};
use calcard::vcard::VCardVersion;
use std::ops::Range;
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.as_bytes()) {
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;
},
"Accept" => {
let mut preferred: Option<(f32, VCardVersion)> = None;
for entry in value.split(',') {
let mut parts = entry.split(';');
if !parts.next().is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case("text/vcard")) {
continue;
}
let mut version = None;
let mut quality = 1.0;
for param in parts {
let Some((name, param_value)) = param.split_once('=') else {
continue;
};
let param_value = param_value.trim().trim_matches('"');
if name.trim().eq_ignore_ascii_case("version") {
version = VCardVersion::try_parse(param_value);
} else if name.trim().eq_ignore_ascii_case("q") {
quality = param_value.parse().unwrap_or(1.0);
}
}
if let Some(version) = version
&& quality > 0.0
&& preferred.is_none_or(|(preferred_quality, _)| quality > preferred_quality) {
preferred = Some((quality, version));
}
}
if let Some((_, version)) = preferred {
self.vcard_version = Some(version);
}
return true;
},
"If-Schedule-Tag-Match" => {
self.if_schedule_tag = value.trim().trim_matches('"').parse().ok();
return true;
},
"Schedule-Reply" => {
self.no_schedule_reply = value == "F";
return true;
},
"Range" => {
self.range = ByteRange::parse(value);
return self.range.is_some();
},
"If-Range" => {
self.if_range = Some(value.trim());
return true;
},
_ => {}
);
false
}
pub fn has_if(&self) -> bool {
!self.if_.is_empty()
}
pub fn eval_if_resources(&self) -> impl Iterator<Item = &str> {
self.if_.iter().filter_map(|if_| if_.resource)
}
pub fn eval_if<T>(&self, resources: &[ResourceState<T>]) -> bool
where
T: AsRef<str>,
{
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,
});
}
}
pub fn eval_if_range(&self, etag: &str, last_modified: Option<&str>) -> bool {
match self.if_range {
Some(validator) => {
!validator.starts_with("W/")
&& (validator == etag
|| last_modified.is_some_and(|last_modified| validator == last_modified))
}
None => true,
}
}
pub fn base_uri(&self) -> Option<&str> {
dav_base_uri(self.uri)
}
}
impl ByteRange {
pub fn parse(value: &str) -> Option<Self> {
let (unit, spec) = value.split_once('=')?;
if !unit.trim().eq_ignore_ascii_case("bytes") || spec.contains(',') {
return None;
}
let (start, end) = spec.split_once('-')?;
let (start, end) = (start.trim(), end.trim());
if !start.is_empty() {
let start = start.parse::<u64>().ok()?;
let end = if !end.is_empty() {
let end = end.parse::<u64>().ok()?;
if end < start {
return None;
}
Some(end)
} else {
None
};
Some(ByteRange::Offset { start, end })
} else {
end.parse::<u64>().ok().map(ByteRange::Suffix)
}
}
pub fn resolve(&self, size: u64) -> Option<Range<u64>> {
match self {
ByteRange::Offset { start, end } if *start < size => {
Some(*start..end.map_or(size, |end| std::cmp::min(end.saturating_add(1), size)))
}
ByteRange::Suffix(length) if *length > 0 && size > 0 => {
Some(size.saturating_sub(*length)..size)
}
_ => None,
}
}
}
pub fn dav_base_uri(uri: &str) -> Option<&str> {
// From a path ../dav/collection/account/..
// returns ../dav/collection/account without the trailing slash
let uri = 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
}
}
impl Depth {
pub fn parse(value: &[u8]) -> Option<Self> {
hashify::tiny_map!(value,
"0" => Depth::Zero,
"1" => Depth::One,
"infinity" => Depth::Infinity,
"infinite" => 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 parse_accept_vcard_version() {
for (header, expected) in [
("text/vcard", None),
("application/json", None),
("text/vcard; version=3.0", Some(VCardVersion::V3_0)),
("text/vcard;version=\"3.0\"", Some(VCardVersion::V3_0)),
("text/vcard; VERSION=3.0", Some(VCardVersion::V3_0)),
("text/vcard; version=3.0; q=1.0", Some(VCardVersion::V3_0)),
(
"text/vcard; version=3.0; charset=utf-8",
Some(VCardVersion::V3_0),
),
(
"text/vcard; charset=utf-8; version=3.0",
Some(VCardVersion::V3_0),
),
(
"text/vcard; version=4.0, text/vcard; version=3.0",
Some(VCardVersion::V4_0),
),
(
"text/vcard; version=4.0; q=0.5, text/vcard; version=3.0",
Some(VCardVersion::V3_0),
),
(
"text/vcard; q=0.5; version=4.0, text/vcard; q=1.0; version=3.0",
Some(VCardVersion::V3_0),
),
(
"text/vcard; version=4.0; q=0, text/vcard; version=3.0; q=0.1",
Some(VCardVersion::V3_0),
),
("*/*, text/vcard; version=3.0", Some(VCardVersion::V3_0)),
("text/vcard-custom; version=3.0", None),
] {
let mut headers = RequestHeaders::new("/dav/card/test/default/");
assert!(headers.parse("Accept", header));
assert_eq!(headers.vcard_version, expected, "failed for {header:?}");
}
}
#[test]
fn parse_range() {
for (header, expected) in [
(
"bytes=0-499",
Some(ByteRange::Offset {
start: 0,
end: Some(499),
}),
),
(
"bytes=500-",
Some(ByteRange::Offset {
start: 500,
end: None,
}),
),
(
"bytes = 500 - 999 ",
Some(ByteRange::Offset {
start: 500,
end: Some(999),
}),
),
(
"BYTES=0-0",
Some(ByteRange::Offset {
start: 0,
end: Some(0),
}),
),
("bytes=-500", Some(ByteRange::Suffix(500))),
("bytes=-0", Some(ByteRange::Suffix(0))),
("bytes=0-499,600-999", None),
("bytes=499-100", None),
("bytes=abc-def", None),
("bytes=-", None),
("bytes=0", None),
("items=0-499", None),
("0-499", None),
] {
let mut headers = RequestHeaders::new("/dav/file/test/file.txt");
assert_eq!(headers.parse("Range", header), expected.is_some());
assert_eq!(headers.range, expected, "failed for {header:?}");
}
for (range, size, expected) in [
(
ByteRange::Offset {
start: 0,
end: Some(499),
},
1000,
Some(0..500),
),
(
ByteRange::Offset {
start: 0,
end: Some(499),
},
100,
Some(0..100),
),
(
ByteRange::Offset {
start: 500,
end: None,
},
1000,
Some(500..1000),
),
(
ByteRange::Offset {
start: 999,
end: Some(u64::MAX),
},
1000,
Some(999..1000),
),
(
ByteRange::Offset {
start: 1000,
end: None,
},
1000,
None,
),
(
ByteRange::Offset {
start: 0,
end: None,
},
0,
None,
),
(ByteRange::Suffix(500), 1000, Some(500..1000)),
(ByteRange::Suffix(5000), 1000, Some(0..1000)),
(ByteRange::Suffix(0), 1000, None),
(ByteRange::Suffix(500), 0, None),
] {
assert_eq!(
range.resolve(size),
expected,
"failed for {range:?} of {size}"
);
}
}
#[test]
fn eval_if_range_header() {
const LAST_MODIFIED: &str = "Mon, 10 Aug 2026 12:00:00 GMT";
let mut headers = RequestHeaders::new("/dav/file/test/file.txt");
assert!(headers.eval_if_range("\"etag\"", Some(LAST_MODIFIED)));
for (validator, expected) in [
("\"etag\"", true),
(LAST_MODIFIED, true),
("W/\"etag\"", false),
("\"other\"", false),
("Sun, 09 Aug 2026 12:00:00 GMT", false),
] {
assert!(headers.parse("If-Range", validator));
assert_eq!(
headers.eval_if_range("\"etag\"", Some(LAST_MODIFIED)),
expected,
"failed for {validator:?}"
);
}
for (validator, expected) in [("\"etag\"", true), (LAST_MODIFIED, false)] {
assert!(headers.parse("If-Range", validator));
assert_eq!(
headers.eval_if_range("\"etag\"", None),
expected,
"failed for {validator:?}"
);
}
}
#[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#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
["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 <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
<urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)"#,
));
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#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
(Not <DAV:no-lock>)"#
));
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", "<urn:uuid:1234>"));
assert_eq!(headers.lock_token, Some("urn:uuid:1234"));
for (input, expected) in [
(
"<urn:uuid:1234>(<urn:uuid:1234>)",
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#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
["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 <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
<urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)"#,
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#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
(Not <DAV:no-lock>)"#,
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#"</resource1>
(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>
[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#"<http://www.example.com/specs/>
(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)"#,
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#"</specs/rfc2518.doc> (["4217"])"#,
vec![If {
resource: "/specs/rfc2518.doc".into(),
list: vec![Condition::ETag {
is_not: false,
tag: "\"4217\"",
}],
}],
),
(
r#"</specs/rfc2518.doc> (Not ["4217"])"#,
vec![If {
resource: "/specs/rfc2518.doc".into(),
list: vec![Condition::ETag {
is_not: true,
tag: "\"4217\"",
}],
}],
),
(
r#"</test/file.txt> (["1234"]) </specs/rfc2518.doc> (Not ["4217"])"#,
vec![
If {
resource: "/test/file.txt".into(),
list: vec![Condition::ETag {
is_not: false,
tag: "\"1234\"",
}],
},
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);
}
}
+210
View File
@@ -0,0 +1,210 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
borrow::Cow,
fmt::{Display, Formatter},
};
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(Box<quick_xml::Error>),
UnexpectedToken(Box<UnexpectedToken>),
}
#[derive(Debug, Clone)]
pub struct UnexpectedToken {
pub expected: Option<Token<'static>>,
pub found: Token<'static>,
}
pub type Result<T> = std::result::Result<T, Error>;
#[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 element: BytesStart<'x>,
pub namespace: Option<Cow<'static, [u8]>>,
}
pub trait DavParser: Sized {
fn parse(stream: &mut Tokenizer<'_>) -> Result<Self>;
}
pub trait XmlValueParser: Sized {
fn parse_bytes(bytes: &[u8]) -> Option<Self>;
fn parse_str(text: &str) -> Option<Self>;
}
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,
}
}
pub fn calendarserver(element: Element) -> NamedElement {
NamedElement {
ns: Namespace::CalendarServer,
element,
}
}
}
impl Token<'_> {
pub fn into_owned(self) -> Token<'static> {
match self {
Token::ElementStart { name, raw } => Token::ElementStart {
name,
raw: raw.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(raw.into_owned()),
Token::Eof => Token::Eof,
}
}
pub fn into_unexpected(self) -> Error {
Error::UnexpectedToken(Box::new(UnexpectedToken {
expected: None,
found: self.into_owned(),
}))
}
}
impl<'x> RawElement<'x> {
pub fn new(element: BytesStart<'x>) -> Self {
RawElement {
element,
namespace: None,
}
}
pub fn with_namespace(self, namespace: quick_xml::name::Namespace<'_>) -> Self {
RawElement {
element: self.element,
namespace: Some(Cow::Owned(namespace.into_inner().to_vec())),
}
}
pub fn with_namespace_static(self, namespace: &'static [u8]) -> Self {
RawElement {
element: self.element,
namespace: Some(Cow::Borrowed(namespace)),
}
}
pub fn into_owned(self) -> RawElement<'static> {
RawElement {
element: self.element.into_owned(),
namespace: self.namespace,
}
}
}
#[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
.element
.attributes_raw()
.trim_ascii()
.eq_ignore_ascii_case(r_raw.element.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)) => {
let l0: &[u8] = l0.element.as_ref();
let r0: &[u8] = r0.element.as_ref();
l0.eq_ignore_ascii_case(r0)
}
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
impl NamedElement {
pub fn into_unexpected(self) -> Error {
Error::UnexpectedToken(Box::new(UnexpectedToken {
expected: None,
found: Token::ElementStart {
name: self,
raw: RawElement::new(BytesStart::new("")),
},
}))
}
}
impl Default for RawElement<'_> {
fn default() -> Self {
RawElement::new(BytesStart::new(""))
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::Xml(err) => write!(f, "XML error: {}", err),
Error::UnexpectedToken(err) => {
write!(f, "Unexpected token: {:?}", err.found)?;
if let Some(expected) = &err.expected {
write!(f, ", expected: {expected:?}")?;
}
Ok(())
}
}
}
}
+741
View File
@@ -0,0 +1,741 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{DavParser, RawElement, Token, XmlValueParser, tokenizer::Tokenizer};
use crate::schema::{
Attribute, AttributeValue, Element, NamedElement, Namespace,
property::{
CalDavProperty, CalDavPropertyName, CalendarData, CardDavProperty, CardDavPropertyName,
Comp, DavProperty, DavValue, PrincipalProperty, ResourceType, WebDavProperty,
},
request::{DavPropertyValue, VCardPropertyWithGroup},
response::List,
};
use calcard::{
Entry, Parser,
common::{IanaParse, PartialDateTime},
icalendar::{ICalendar, ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::{VCardParameterName, VCardProperty, VCardVersion},
};
use mail_parser::DateTime;
use types::{TimeRange, dead_property::DeadProperty};
impl Tokenizer<'_> {
pub(crate) fn collect_properties(
&mut self,
mut elements: Vec<DavProperty>,
) -> crate::parser::Result<Vec<DavProperty>> {
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,
},
raw,
} => {
let mut version = None;
for attribute in raw.attributes::<VCardPropertyWithGroup>() {
if let Attribute::Version(value) = attribute? {
version = VCardVersion::try_parse(value.trim().trim_matches('"'));
}
}
elements.push(DavProperty::CardDav(CardDavProperty::AddressData {
properties: self.collect_address_data()?,
version,
}));
}
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<CalendarData> {
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<ICalendarComponentType> = 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().cloned() {
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::<ICalendarComponentType>() {
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::<ICalendarProperty>() {
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().cloned(),
name: Some(name),
no_value,
});
}
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::Expand,
},
raw,
} => {
data.expand = TimeRange::from_raw(&raw)?;
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::LimitRecurrenceSet,
},
raw,
} => {
data.limit_recurrence = TimeRange::from_raw(&raw)?;
self.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::LimitFreebusySet,
},
raw,
} => {
data.limit_freebusy = TimeRange::from_raw(&raw)?;
self.expect_element_end()?;
}
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
if let Some(last_component) = components.pop()
&& 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<Vec<CardDavPropertyName>> {
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::<VCardPropertyWithGroup>() {
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,
elements: &mut Vec<DavPropertyValue>,
) -> crate::parser::Result<()> {
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::<DateTime>()? {
Some(Ok(value)) => DavValue::Timestamp(value.to_timestamp()),
Some(Err(value)) => DavValue::String(value),
None => DavValue::Null,
}
}
DavProperty::CalDav(CalDavProperty::CalendarTimezone) => {
match self
.collect_string_value()?
.map(|v| ICalendar::parse(&v).map_err(|_| v))
{
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::<ICalendarComponentType>()
{
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(())
}
}
pub(crate) trait TimeRangeFromRaw {
fn from_raw(raw: &RawElement<'_>) -> super::Result<Option<TimeRange>>;
}
impl TimeRangeFromRaw for TimeRange {
fn from_raw(raw: &RawElement<'_>) -> super::Result<Option<Self>> {
let mut range = TimeRange {
start: i64::MIN,
end: i64::MAX,
};
for attribute in raw.attributes::<ICalendarDateTime>() {
match attribute? {
Attribute::Start(start) => {
range.start = start.0;
}
Attribute::End(end) => {
range.end = end.0;
}
_ => {}
}
}
if range.end < range.start {
range.end = i64::MAX;
}
if range.start != i64::MIN || range.end != i64::MAX {
Ok(Some(range))
} else {
Ok(None)
}
}
}
impl DavProperty {
pub(crate) fn from_element(element: NamedElement) -> Option<Self> {
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::Principal(PrincipalProperty::AlternateURISet))
}
(Namespace::Dav, Element::PrincipalUrl) => {
Some(DavProperty::Principal(PrincipalProperty::PrincipalURL))
}
(Namespace::Dav, Element::GroupMemberSet) => {
Some(DavProperty::Principal(PrincipalProperty::GroupMemberSet))
}
(Namespace::Dav, Element::GroupMembership) => {
Some(DavProperty::Principal(PrincipalProperty::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::AddressbookHomeSet) => Some(DavProperty::Principal(
PrincipalProperty::AddressbookHomeSet,
)),
(Namespace::CardDav, Element::PrincipalAddress) => {
Some(DavProperty::Principal(PrincipalProperty::PrincipalAddress))
}
(Namespace::CardDav, Element::AddressData) => {
Some(DavProperty::CardDav(CardDavProperty::AddressData {
properties: Default::default(),
version: None,
}))
}
(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::ScheduleDefaultCalendarUrl) => Some(DavProperty::CalDav(
CalDavProperty::ScheduleDefaultCalendarURL,
)),
(Namespace::CalDav, Element::ScheduleTag) => {
Some(DavProperty::CalDav(CalDavProperty::ScheduleTag))
}
(Namespace::CalDav, Element::ScheduleCalendarTransp) => {
Some(DavProperty::CalDav(CalDavProperty::ScheduleCalendarTransp))
}
(Namespace::CalDav, Element::CalendarHomeSet) => {
Some(DavProperty::Principal(PrincipalProperty::CalendarHomeSet))
}
(Namespace::CalDav, Element::CalendarUserAddressSet) => Some(DavProperty::Principal(
PrincipalProperty::CalendarUserAddressSet,
)),
(Namespace::CalDav, Element::CalendarUserType) => {
Some(DavProperty::Principal(PrincipalProperty::CalendarUserType))
}
(Namespace::CalDav, Element::ScheduleInboxUrl) => {
Some(DavProperty::Principal(PrincipalProperty::ScheduleInboxURL))
}
(Namespace::CalDav, Element::ScheduleOutboxUrl) => {
Some(DavProperty::Principal(PrincipalProperty::ScheduleOutboxURL))
}
(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))
}
(Namespace::CalendarServer, Element::Getctag) => {
Some(DavProperty::WebDav(WebDavProperty::GetCTag))
}
_ => None,
}
}
}
impl TryFrom<NamedElement> for ResourceType {
type Error = ();
fn try_from(value: NamedElement) -> Result<Self, Self::Error> {
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),
(Namespace::CalDav, Element::ScheduleInbox) => Ok(ResourceType::ScheduleInbox),
(Namespace::CalDav, Element::ScheduleOutbox) => Ok(ResourceType::ScheduleOutbox),
_ => Err(()),
}
}
}
struct ICalendarDateTime(i64);
impl AttributeValue for ICalendarDateTime {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
let mut dt = PartialDateTime::default();
dt.parse_timestamp(&mut s.as_bytes().iter().peekable(), true);
dt.to_timestamp().map(ICalendarDateTime)
}
}
impl AttributeValue for ICalendarComponentType {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
ICalendarComponentType::parse(s.as_bytes())
}
}
impl AttributeValue for ICalendarProperty {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
ICalendarProperty::parse(s.as_bytes())
.unwrap_or_else(|| ICalendarProperty::Other(s.to_string()))
.into()
}
}
impl AttributeValue for ICalendarParameterName {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
ICalendarParameterName::parse(s).into()
}
}
impl AttributeValue for VCardPropertyWithGroup {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
if let Some((group, s)) = s.split_once('.') {
VCardPropertyWithGroup {
name: VCardProperty::parse(s.as_bytes())
.unwrap_or_else(|| VCardProperty::Other(s.to_string())),
group: group.to_string().into(),
}
.into()
} else {
VCardPropertyWithGroup {
name: VCardProperty::parse(s.as_bytes())
.unwrap_or_else(|| VCardProperty::Other(s.to_string())),
group: None,
}
.into()
}
}
}
impl AttributeValue for VCardParameterName {
fn from_str(s: &str) -> Option<Self>
where
Self: Sized,
{
VCardParameterName::parse(s).into()
}
}
impl XmlValueParser for ICalendar {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
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<Self> {
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<Self> {
std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok())
}
fn parse_str(text: &str) -> Option<Self> {
text.parse().ok()
}
}
impl XmlValueParser for u32 {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
std::str::from_utf8(bytes).ok().and_then(|s| s.parse().ok())
}
fn parse_str(text: &str) -> Option<Self> {
text.parse().ok()
}
}
impl XmlValueParser for DateTime {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
std::str::from_utf8(bytes)
.ok()
.and_then(DateTime::parse_rfc3339)
}
fn parse_str(text: &str) -> Option<Self> {
DateTime::parse_rfc3339(text)
}
}
+503
View File
@@ -0,0 +1,503 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{Error, RawElement, Token, UnexpectedToken, XmlValueParser};
use crate::schema::{Attribute, AttributeValue, Element, NamedElement, Namespace};
use quick_xml::{
NsReader, XmlVersion,
events::{Event, attributes::AttrError},
name::ResolveResult,
};
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();
Self {
xml,
last_is_end: false,
}
}
pub fn token(&'_ mut self) -> super::Result<Token<'_>> {
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
.xml_content(XmlVersion::Implicit1_0)
.map(Token::Text)
.map_err(|err| Error::Xml(Box::new(err.into())));
}
Event::GeneralRef(entity) => {
let entity_ref: &[u8] = entity.as_ref();
hashify::fnc_map!(entity_ref,
b"lt" => { return Ok(Token::Text("<".into())); },
b"gt" => { return Ok(Token::Text(">".into())); },
b"amp" => { return Ok(Token::Text("&".into())); },
b"apos" => { return Ok(Token::Text("'".into())); },
b"quot" => { return Ok(Token::Text("\"".into())); },
_ => {
if let Ok(Some(gr)) = entity.resolve_char_ref() {
return Ok(Token::Text(gr.to_string().into()));
}
}
);
return entity
.xml_content(XmlVersion::Implicit1_0)
.map(Token::Text)
.map_err(|err| Error::Xml(Box::new(err.into())));
}
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(raw_ns) if !raw_ns.as_ref().is_empty() => {
if let (Some(ns), Some(element)) = (
Namespace::try_parse(raw_ns.as_ref()),
Element::try_parse(name.local_name().as_ref()).copied(),
) {
return Ok(Token::ElementStart {
name: NamedElement { ns, element },
raw: RawElement::new(tag)
.with_namespace_static(ns.namespace().as_bytes()),
});
} else {
return Ok(Token::UnknownElement(
RawElement::new(tag).with_namespace(raw_ns),
));
}
}
ResolveResult::Unknown(p) => {
return Err(Error::Xml(Box::new(quick_xml::Error::Namespace(
quick_xml::name::NamespaceError::UnknownPrefix(p),
))));
}
_ => {
return Ok(Token::UnknownElement(RawElement::new(tag)));
}
}
}
}
pub fn unwrap_named_element(&mut self) -> super::Result<NamedElement> {
match self.token()? {
Token::ElementStart { name, .. } => Ok(name),
found => Err(Error::UnexpectedToken(Box::new(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(Box::new(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<bool> {
match self.token()? {
Token::ElementStart { name, .. } if name == expected => Ok(true),
Token::Eof => Ok(false),
found => Err(Error::UnexpectedToken(Box::new(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(Box::new(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<Option<String>> {
let mut depth = 1;
let mut value: Option<String> = None;
loop {
match self.token()? {
Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1,
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
}
Token::Text(text) => {
if let Some(ref mut v) = value {
v.push_str(&text);
} else {
value = Some(text.into_owned());
}
}
Token::Bytes(bytes) => {
if let Some(ref mut v) = value {
v.push_str(&String::from_utf8_lossy(&bytes));
} else {
value = Some(String::from_utf8_lossy(&bytes).into_owned());
}
}
Token::Eof => return Err(Token::Eof.into_unexpected()),
}
}
Ok(value)
}
pub fn parse_value<T: XmlValueParser>(&mut self) -> super::Result<Option<Result<T, String>>> {
let mut depth = 1;
let mut result: Option<Result<T, String>> = None;
loop {
match self.token()? {
Token::ElementStart { .. } | Token::UnknownElement(_) => depth += 1,
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
}
Token::Text(text) => {
if let Some(value) = T::parse_str(&text) {
result = Some(Ok(value));
} else {
result = Some(Err(text.into_owned()));
}
}
Token::Bytes(bytes) => {
if let Some(value) = T::parse_bytes(&bytes) {
result = Some(Ok(value));
} else {
result = Some(Err(String::from_utf8_lossy(&bytes).into_owned()));
}
}
Token::Eof => return Err(Token::Eof.into_unexpected()),
}
}
Ok(result)
}
pub fn collect_elements<T>(&mut self) -> super::Result<Vec<T>>
where
T: TryFrom<NamedElement>,
{
let mut elements = Vec::with_capacity(2);
let mut depth = 1;
loop {
match self.token()? {
Token::ElementStart { name, .. } => {
if depth == 1
&& let Ok(element) = T::try_from(name)
{
elements.push(element);
}
depth += 1;
}
Token::UnknownElement(_) => {
depth += 1;
}
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
}
Token::Eof => break,
_ => {}
}
}
Ok(elements)
}
}
impl RawElement<'_> {
pub fn attributes<T: AttributeValue>(
&self,
) -> impl Iterator<Item = super::Result<Attribute<T>>> + '_ {
self.element.attributes().filter_map(|attr| match attr {
Ok(attr) => match attr.normalized_value(XmlVersion::Implicit1_0) {
Ok(value) => Attribute::from_param(attr.key.as_ref(), value).map(Ok),
Err(err) => Some(Err(err.into())),
},
Err(err) => Some(Err(err.into())),
})
}
}
impl From<quick_xml::Error> for Error {
fn from(err: quick_xml::Error) -> Self {
Error::Xml(Box::new(err))
}
}
impl From<AttrError> for Error {
fn from(err: AttrError) -> Self {
Error::Xml(Box::new(err.into()))
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use crate::schema::{Collation, MatchType};
use super::*;
#[derive(Debug, PartialEq, Eq)]
pub enum TestToken<'x> {
ElementStart(NamedElement),
ElementEnd,
Attribute(Attribute<String>),
Bytes(Cow<'x, [u8]>),
Text(Cow<'x, str>),
}
#[test]
fn test_tokenizer() {
for (input, expected) in [
(
r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
<C:calendar-data/>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR"/>
</C:filter>
</C:calendar-query>"#,
vec![
TestToken::ElementStart(NamedElement {
ns: Namespace::CalDav,
element: Element::CalendarQuery,
}),
TestToken::ElementStart(NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
}),
TestToken::ElementStart(NamedElement {
ns: Namespace::Dav,
element: Element::Getetag,
}),
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CalDav,
element: Element::CalendarData,
}),
TestToken::ElementEnd,
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CalDav,
element: Element::Filter,
}),
TestToken::ElementStart(NamedElement {
ns: Namespace::CalDav,
element: Element::CompFilter,
}),
TestToken::Attribute(Attribute::Name("VCALENDAR".to_string())),
TestToken::ElementEnd,
TestToken::ElementEnd,
TestToken::ElementEnd,
],
),
(
r#" <?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<D:getetag/>
<C:address-data>
<C:prop name="VERSION"/>
<C:prop name="UID"/>
<C:prop name="NICKNAME"/>
<C:prop name="EMAIL"/>
<C:prop name="FN"/>
</C:address-data>
</D:prop>
<C:filter>
<C:prop-filter name="NICKNAME">
<C:text-match collation="i;unicode-casemap"
match-type="equals"
>me</C:text-match>
</C:prop-filter>
</C:filter>
</C:addressbook-query>"#,
vec![
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::AddressbookQuery,
}),
TestToken::ElementStart(NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
}),
TestToken::ElementStart(NamedElement {
ns: Namespace::Dav,
element: Element::Getetag,
}),
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::AddressData,
}),
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::Prop,
}),
TestToken::Attribute(Attribute::Name("VERSION".to_string())),
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::Prop,
}),
TestToken::Attribute(Attribute::Name("UID".to_string())),
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::Prop,
}),
TestToken::Attribute(Attribute::Name("NICKNAME".to_string())),
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::Prop,
}),
TestToken::Attribute(Attribute::Name("EMAIL".to_string())),
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::Prop,
}),
TestToken::Attribute(Attribute::Name("FN".to_string())),
TestToken::ElementEnd,
TestToken::ElementEnd,
TestToken::ElementEnd,
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::Filter,
}),
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::PropFilter,
}),
TestToken::Attribute(Attribute::Name("NICKNAME".to_string())),
TestToken::ElementStart(NamedElement {
ns: Namespace::CardDav,
element: Element::TextMatch,
}),
TestToken::Attribute(Attribute::Collation(Collation::UnicodeCasemap)),
TestToken::Attribute(Attribute::MatchType(MatchType::Equals)),
TestToken::Text("me".into()),
TestToken::ElementEnd,
TestToken::ElementEnd,
TestToken::ElementEnd,
TestToken::ElementEnd,
],
),
] {
let mut tokenizer = Tokenizer::new(input.as_bytes());
let mut result = vec![];
loop {
match tokenizer.token() {
Ok(token) => match token {
Token::ElementStart { name, raw } => {
result.push(TestToken::ElementStart(name));
for attr in raw.attributes::<String>() {
result.push(TestToken::Attribute(attr.unwrap()));
}
}
Token::ElementEnd => {
result.push(TestToken::ElementEnd);
}
Token::Bytes(cow) => {
result.push(TestToken::Bytes(cow.into_owned().into()));
}
Token::Text(cow) => {
result.push(TestToken::Text(cow.into_owned().into()));
}
Token::UnknownElement(_) => {
//result.push(TestToken::UnknownElement(unknown_element));
}
Token::Eof => break,
},
Err(err) => {
panic!("Error: {:?}", err);
}
}
}
assert_eq!(result, expected);
}
}
}
+469
View File
@@ -0,0 +1,469 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
parser::{DavParser, Token, tokenizer::Tokenizer},
schema::{
Element, NamedElement, Namespace,
property::{DavValue, Privilege},
request::{
Acl, AclPrincipalPropSet, DavPropertyValue, PrincipalMatch, PrincipalMatchProperties,
PrincipalPropertySearch, PropertySearch,
},
response::{Ace, GrantDeny, Href, List, Principal},
},
};
impl DavParser for Acl {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
stream.expect_named_element(NamedElement::dav(Element::Acl))?;
let mut acl = Acl { aces: vec![] };
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Ace,
},
..
} => {
acl.aces.push(Ace::parse(stream)?);
}
Token::ElementEnd => {
break;
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
other => {
return Err(other.into_unexpected());
}
}
}
Ok(acl)
}
}
impl DavParser for Ace {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut ace = Ace {
principal: Principal::All,
invert: false,
grant_deny: GrantDeny::Grant(List(vec![])),
protected: false,
inherited: None,
};
let mut depth = 1;
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Principal,
},
..
} => {
ace.principal = Principal::parse(stream)?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Invert,
},
..
} if depth == 1 => {
ace.invert = true;
depth += 1;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Protected,
},
..
} if depth == 1 => {
ace.protected = true;
stream.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Inherited,
},
..
} if depth == 1 => {
stream.expect_named_element(NamedElement::dav(Element::Href))?;
ace.inherited = stream.collect_string_value()?.map(Href);
stream.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Grant,
},
..
} if depth == 1 => {
ace.grant_deny = GrantDeny::Grant(List(stream.collect_privileges()?));
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Deny,
},
..
} if depth == 1 => {
ace.grant_deny = GrantDeny::Deny(List(stream.collect_privileges()?));
}
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
other => {
return Err(other.into_unexpected());
}
}
}
Ok(ace)
}
}
impl DavParser for Principal {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let result = match stream.unwrap_named_element()? {
NamedElement {
ns: Namespace::Dav,
element: Element::Href,
} => Principal::Href(Href(stream.collect_string_value()?.unwrap_or_default())),
NamedElement {
ns: Namespace::Dav,
element: Element::All,
} => {
stream.expect_element_end()?;
Principal::All
}
NamedElement {
ns: Namespace::Dav,
element: Element::Authenticated,
} => {
stream.expect_element_end()?;
Principal::Authenticated
}
NamedElement {
ns: Namespace::Dav,
element: Element::Unauthenticated,
} => {
stream.expect_element_end()?;
Principal::Unauthenticated
}
NamedElement {
ns: Namespace::Dav,
element: Element::Property,
} => {
let property = stream.collect_properties(Vec::new())?;
Principal::Property(List(
property
.into_iter()
.map(|prop| DavPropertyValue::new(prop, DavValue::Null))
.collect(),
))
}
NamedElement {
ns: Namespace::Dav,
element: Element::Self_,
} => {
stream.expect_element_end()?;
Principal::Self_
}
other => return Err(other.into_unexpected()),
};
stream.expect_element_end()?;
Ok(result)
}
}
impl Tokenizer<'_> {
pub fn collect_privileges(&mut self) -> crate::parser::Result<Vec<Privilege>> {
let mut privileges = Vec::new();
let mut depth = 1;
loop {
match self.token()? {
Token::ElementStart { name, .. } => {
if let Some(privilege) = Privilege::from_element(name) {
privileges.push(privilege);
self.expect_element_end()?;
} else {
depth += 1;
}
}
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
}
Token::UnknownElement(_) => {
self.seek_element_end()?;
}
other => {
return Err(other.into_unexpected());
}
}
}
Ok(privileges)
}
}
impl Privilege {
pub fn from_element(element: NamedElement) -> Option<Self> {
match (element.ns, element.element) {
(Namespace::Dav, Element::Read) => Some(Privilege::Read),
(Namespace::Dav, Element::Write) => Some(Privilege::Write),
(Namespace::Dav, Element::WriteProperties) => Some(Privilege::WriteProperties),
(Namespace::Dav, Element::WriteContent) => Some(Privilege::WriteContent),
(Namespace::Dav, Element::Unlock) => Some(Privilege::Unlock),
(Namespace::Dav, Element::ReadAcl) => Some(Privilege::ReadAcl),
(Namespace::Dav, Element::ReadCurrentUserPrivilegeSet) => {
Some(Privilege::ReadCurrentUserPrivilegeSet)
}
(Namespace::Dav, Element::WriteAcl) => Some(Privilege::WriteAcl),
(Namespace::Dav, Element::Bind) => Some(Privilege::Bind),
(Namespace::Dav, Element::Unbind) => Some(Privilege::Unbind),
(Namespace::Dav, Element::All) => Some(Privilege::All),
(Namespace::CalDav, Element::ReadFreeBusy) => Some(Privilege::ReadFreeBusy),
(Namespace::CalDav, Element::ScheduleDeliver) => Some(Privilege::ScheduleDeliver),
(Namespace::CalDav, Element::ScheduleDeliverInvite) => {
Some(Privilege::ScheduleDeliverInvite)
}
(Namespace::CalDav, Element::ScheduleDeliverReply) => {
Some(Privilege::ScheduleDeliverReply)
}
(Namespace::CalDav, Element::ScheduleQueryFreebusy) => {
Some(Privilege::ScheduleQueryFreeBusy)
}
(Namespace::CalDav, Element::ScheduleSend) => Some(Privilege::ScheduleSend),
(Namespace::CalDav, Element::ScheduleSendInvite) => Some(Privilege::ScheduleSendInvite),
(Namespace::CalDav, Element::ScheduleSendReply) => Some(Privilege::ScheduleSendReply),
(Namespace::CalDav, Element::ScheduleSendFreebusy) => {
Some(Privilege::ScheduleSendFreeBusy)
}
_ => None,
}
}
}
impl DavParser for AclPrincipalPropSet {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut acps = AclPrincipalPropSet { properties: vec![] };
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
},
..
} => {
acps.properties = stream.collect_properties(acps.properties)?;
}
Token::ElementEnd => {
break;
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
other => {
return Err(other.into_unexpected());
}
}
}
Ok(acps)
}
}
impl DavParser for PrincipalMatch {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut pm = PrincipalMatch {
principal_properties: PrincipalMatchProperties::Self_,
properties: vec![],
};
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::PrincipalProperty,
},
..
} => {
pm.principal_properties = PrincipalMatchProperties::Properties(
stream.collect_properties(Vec::new())?,
);
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Self_,
},
..
} => {
pm.principal_properties = PrincipalMatchProperties::Self_;
stream.expect_element_end()?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
},
..
} => {
pm.properties = stream.collect_properties(pm.properties)?;
}
Token::ElementEnd => {
break;
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
other => {
return Err(other.into_unexpected());
}
}
}
Ok(pm)
}
}
impl DavParser for PrincipalPropertySearch {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut pps = PrincipalPropertySearch {
property_search: vec![],
properties: vec![],
apply_to_principal_collection_set: false,
};
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::PropertySearch,
},
..
} => {
if let Some(prop) = PropertySearch::parse(stream)? {
pps.property_search.push(prop);
}
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
},
..
} => {
pps.properties = stream.collect_properties(pps.properties)?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::ApplyToPrincipalCollectionSet,
},
..
} => {
stream.expect_element_end()?;
pps.apply_to_principal_collection_set = true;
}
Token::ElementEnd => {
break;
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
other => {
return Err(other.into_unexpected());
}
}
}
Ok(pps)
}
}
impl PropertySearch {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Option<Self>> {
let mut property = None;
let mut match_ = None;
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
},
..
} => {
property = stream.collect_properties(Vec::new())?.into_iter().next();
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Match,
},
..
} => {
match_ = stream.collect_string_value()?;
}
Token::ElementEnd => {
break;
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
other => {
return Err(other.into_unexpected());
}
}
}
Ok(property.map(|property| PropertySearch {
property,
match_: match_.unwrap_or_default(),
}))
}
}
+110
View File
@@ -0,0 +1,110 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
parser::{DavParser, Token, tokenizer::Tokenizer},
schema::{
Element, NamedElement, Namespace,
property::{LockScope, LockType},
request::LockInfo,
},
};
use types::dead_property::DeadProperty;
impl DavParser for LockInfo {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut lockinfo = LockInfo {
lock_scope: LockScope::Exclusive,
lock_type: LockType::Write,
owner: None,
};
if stream.expect_named_element_or_eof(NamedElement::dav(Element::Lockinfo))? {
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Lockscope,
},
..
} => {
lockinfo.lock_scope = LockScope::parse(stream)?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Locktype,
},
..
} => {
lockinfo.lock_type = LockType::parse(stream)?;
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Owner,
},
..
} => {
lockinfo.owner = Some(DeadProperty::parse(stream)?);
}
Token::ElementEnd | Token::Eof => {
break;
}
other => {
return Err(other.into_unexpected());
}
}
}
}
Ok(lockinfo)
}
}
impl DavParser for LockScope {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
match stream.unwrap_named_element()? {
NamedElement {
ns: Namespace::Dav,
element: Element::Exclusive,
} => {
stream.expect_element_end()?;
stream.expect_element_end()?;
Ok(LockScope::Exclusive)
}
NamedElement {
ns: Namespace::Dav,
element: Element::Shared,
} => {
stream.expect_element_end()?;
stream.expect_element_end()?;
Ok(LockScope::Shared)
}
other => Err(other.into_unexpected()),
}
}
}
impl DavParser for LockType {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
match stream.unwrap_named_element()? {
NamedElement {
ns: Namespace::Dav,
element: Element::Write,
} => {
stream.expect_element_end()?;
stream.expect_element_end()?;
Ok(LockType::Write)
}
other => Err(other.into_unexpected()),
}
}
}
+66
View File
@@ -0,0 +1,66 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
parser::{DavParser, Token, tokenizer::Tokenizer},
schema::{Element, NamedElement, Namespace, request::MkCol},
};
impl DavParser for MkCol {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut mkcol = MkCol {
is_mkcalendar: false,
props: Vec::new(),
};
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Mkcol,
},
..
} => {}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::Mkcalendar,
},
..
} => {
mkcol.is_mkcalendar = true;
}
Token::Eof => {
return Ok(mkcol);
}
other => return Err(other.into_unexpected()),
};
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Set,
},
..
} => {
stream.expect_named_element(NamedElement::dav(Element::Prop))?;
stream.collect_property_values(&mut mkcol.props)?;
stream.expect_element_end()?;
}
Token::ElementEnd | Token::Eof => {
break;
}
token => return Err(token.into_unexpected()),
}
}
Ok(mkcol)
}
}
+201
View File
@@ -0,0 +1,201 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
parser::{DavParser, RawElement, Token, tokenizer::Tokenizer},
schema::Namespace,
};
use types::dead_property::{DeadElementTag, DeadProperty, DeadPropertyTag};
pub mod acl;
pub mod lockinfo;
pub mod mkcol;
pub mod propertyupdate;
pub mod propfind;
pub mod report;
impl DavParser for DeadProperty {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut depth = 1;
let mut items = DeadProperty::default();
loop {
match stream.token()? {
Token::ElementStart { raw, .. } | Token::UnknownElement(raw) => {
items.0.push(DeadPropertyTag::ElementStart((&raw).into()));
depth += 1;
}
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
items.0.push(DeadPropertyTag::ElementEnd);
}
Token::Text(text) => {
items.0.push(DeadPropertyTag::Text(text.into_owned()));
}
Token::Bytes(bytes) => {
items.0.push(DeadPropertyTag::Text(
String::from_utf8_lossy(&bytes).into_owned(),
));
}
Token::Eof => {
break;
}
}
}
Ok(items)
}
}
pub trait NsDeadProperty {
fn single_with_ns(namespace: Namespace, name: &str) -> Self;
}
impl NsDeadProperty for DeadProperty {
fn single_with_ns(namespace: Namespace, name: &str) -> Self {
DeadProperty(vec![
DeadPropertyTag::ElementStart(DeadElementTag {
name: format!("{}:{name}", namespace.prefix()),
attrs: None,
}),
DeadPropertyTag::ElementEnd,
])
}
}
impl From<&RawElement<'_>> for DeadElementTag {
fn from(raw: &RawElement<'_>) -> Self {
let name = std::str::from_utf8(raw.element.local_name().as_ref())
.unwrap_or("invalid-utf8")
.trim_ascii()
.to_string();
let mut attrs = String::with_capacity(raw.element.attributes_raw().len());
if let Some(namespace) = &raw.namespace {
attrs.push_str("xmlns=\"");
attrs.push_str(std::str::from_utf8(namespace).unwrap_or("invalid-utf8"));
attrs.push('"');
}
for attr in raw.element.attributes().flatten() {
if attr.key.as_ref() == b"xmlns" || attr.key.as_ref().starts_with(b"xmlns:") {
// Skip namespace attributes
continue;
}
if let (Ok(key), Ok(value)) = (
std::str::from_utf8(attr.key.as_ref()),
std::str::from_utf8(attr.value.as_ref()),
) {
if !attrs.is_empty() {
attrs.push(' ');
}
attrs.push_str(key);
attrs.push('=');
attrs.push('"');
attrs.push_str(value);
attrs.push('"');
}
}
DeadElementTag {
name,
attrs: (!attrs.is_empty()).then_some(attrs),
}
}
}
#[cfg(test)]
mod tests {
use calcard::vcard::VCardVersion;
use crate::{
parser::{DavParser, tokenizer::Tokenizer},
schema::{
property::{CardDavProperty, DavProperty},
request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report},
},
};
#[test]
fn parse_address_data_version() {
let xml = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<C:address-data content-type="text/vcard" version="3.0">
<C:prop name="FN"/>
</C:address-data>
</D:prop>
</C:addressbook-query>"#;
let mut tokenizer = Tokenizer::new(xml.as_bytes());
let report = Report::parse(&mut tokenizer).unwrap();
let Report::AddressbookQuery(query) = report else {
panic!("expected addressbook-query, got {report:?}");
};
let PropFind::Prop(properties) = query.properties else {
panic!("expected prop, got {:?}", query.properties);
};
let version = properties.iter().find_map(|property| match property {
DavProperty::CardDav(CardDavProperty::AddressData { version, .. }) => Some(*version),
_ => None,
});
assert_eq!(version, Some(Some(VCardVersion::V3_0)));
}
#[test]
fn parse_requests() {
for entry in std::fs::read_dir("resources/requests").unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().map(|ext| ext == "xml").unwrap_or(false) {
println!("Parsing: {:?}", path);
let filename = path.file_name().unwrap().to_str().unwrap();
let xml = std::fs::read_to_string(&path).unwrap();
let mut tokenizer = Tokenizer::new(xml.as_bytes());
let json_path = path.with_extension("json");
let json_output = match filename.split_once('-').unwrap().0 {
"propfind" => match PropFind::parse(&mut tokenizer) {
Ok(propfind) => serde_json::to_string_pretty(&propfind).unwrap(),
Err(_) => String::new(),
},
"propertyupdate" => serde_json::to_string_pretty(
&PropertyUpdate::parse(&mut tokenizer).unwrap(),
)
.unwrap(),
"mkcol" => serde_json::to_string_pretty(&MkCol::parse(&mut tokenizer).unwrap())
.unwrap(),
"lockinfo" => {
serde_json::to_string_pretty(&LockInfo::parse(&mut tokenizer).unwrap())
.unwrap()
}
"report" => {
serde_json::to_string_pretty(&Report::parse(&mut tokenizer).unwrap())
.unwrap()
}
"acl" => {
serde_json::to_string_pretty(&Acl::parse(&mut tokenizer).unwrap()).unwrap()
}
_ => {
panic!("Unknown method: {}", filename);
}
};
/*if json_path.exists() {
let expected = std::fs::read_to_string(json_path).unwrap();
assert_eq!(json_output, expected);
} else {*/
std::fs::write(json_path, json_output).unwrap();
//}
}
}
}
}
@@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
parser::{DavParser, Token, tokenizer::Tokenizer},
schema::{Element, NamedElement, Namespace, request::PropertyUpdate},
};
impl DavParser for PropertyUpdate {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
stream.expect_named_element(NamedElement::dav(Element::Propertyupdate))?;
let mut update = PropertyUpdate {
set: Vec::with_capacity(4),
remove: Vec::with_capacity(4),
set_first: true,
};
loop {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Set,
},
..
} => {
stream.expect_named_element(NamedElement::dav(Element::Prop))?;
stream.collect_property_values(&mut update.set)?;
stream.expect_element_end()?;
update.set_first = update.remove.is_empty();
}
Token::ElementStart {
name:
NamedElement {
ns: Namespace::Dav,
element: Element::Remove,
},
..
} => {
stream.expect_named_element(NamedElement::dav(Element::Prop))?;
update.remove = stream.collect_properties(update.remove)?;
stream.expect_element_end()?;
}
Token::ElementEnd | Token::Eof => {
break;
}
Token::UnknownElement(_) => {
// Ignore unknown elements
stream.seek_element_end()?;
}
token => return Err(token.into_unexpected()),
}
}
Ok(update)
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
parser::{DavParser, Token, tokenizer::Tokenizer},
schema::{Element, NamedElement, Namespace, request::PropFind},
};
impl DavParser for PropFind {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
if stream.expect_named_element_or_eof(NamedElement::dav(Element::Propfind))? {
match stream.unwrap_named_element()? {
NamedElement {
ns: Namespace::Dav,
element: Element::Propname,
} => Ok(PropFind::PropName),
NamedElement {
ns: Namespace::Dav,
element: Element::Allprop,
} => {
stream.expect_element_end()?;
if matches!(
stream.token()?,
Token::ElementStart {
name: NamedElement {
ns: Namespace::Dav,
element: Element::Include
},
..
}
) {
stream.collect_properties(Vec::new()).map(PropFind::AllProp)
} else {
Ok(PropFind::AllProp(vec![]))
}
}
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
} => stream.collect_properties(Vec::new()).map(PropFind::Prop),
element => Err(element.into_unexpected()),
}
} else {
Ok(PropFind::AllProp(vec![]))
}
}
}
+641
View File
@@ -0,0 +1,641 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Depth,
parser::{
DavParser, RawElement, Token, XmlValueParser, property::TimeRangeFromRaw,
tokenizer::Tokenizer,
},
schema::{
Attribute, Collation, Element, MatchType, NamedElement, Namespace,
property::DavProperty,
request::{
AclPrincipalPropSet, AddressbookQuery, CalendarQuery, ExpandProperty,
ExpandPropertyItem, Filter, FilterOp, FreeBusyQuery, MultiGet, PrincipalMatch,
PrincipalPropertySearch, PropFind, Report, SyncCollection, TextMatch, Timezone,
VCardPropertyWithGroup,
},
},
};
use calcard::{
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::VCardParameterName,
};
use types::{TimeRange, dead_property::DeadElementTag};
impl DavParser for Report {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
match stream.unwrap_named_element()? {
NamedElement {
ns: Namespace::CalDav,
element: Element::CalendarQuery,
} => CalendarQuery::parse(stream).map(Report::CalendarQuery),
NamedElement {
ns: Namespace::CalDav,
element: Element::FreeBusyQuery,
} => FreeBusyQuery::parse(stream).map(Report::FreeBusyQuery),
NamedElement {
ns: Namespace::CalDav,
element: Element::CalendarMultiget,
} => MultiGet::parse(stream).map(Report::CalendarMultiGet),
NamedElement {
ns: Namespace::CardDav,
element: Element::AddressbookQuery,
} => AddressbookQuery::parse(stream).map(Report::AddressbookQuery),
NamedElement {
ns: Namespace::CardDav,
element: Element::AddressbookMultiget,
} => MultiGet::parse(stream).map(Report::AddressbookMultiGet),
NamedElement {
ns: Namespace::Dav,
element: Element::SyncCollection,
} => SyncCollection::parse(stream).map(Report::SyncCollection),
NamedElement {
ns: Namespace::Dav,
element: Element::AclPrincipalPropSet,
} => AclPrincipalPropSet::parse(stream).map(Report::AclPrincipalPropSet),
NamedElement {
ns: Namespace::Dav,
element: Element::PrincipalMatch,
} => PrincipalMatch::parse(stream).map(Report::PrincipalMatch),
NamedElement {
ns: Namespace::Dav,
element: Element::PrincipalPropertySearch,
} => PrincipalPropertySearch::parse(stream).map(Report::PrincipalPropertySearch),
NamedElement {
ns: Namespace::Dav,
element: Element::PrincipalSearchPropertySet,
} => stream
.expect_element_end()
.map(|_| Report::PrincipalSearchPropertySet),
NamedElement {
ns: Namespace::Dav,
element: Element::ExpandProperty,
} => ExpandProperty::parse(stream).map(Report::ExpandProperty),
other => Err(other.into_unexpected()),
}
}
}
impl DavParser for CalendarQuery {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut cq = CalendarQuery {
properties: PropFind::AllProp(vec![]),
filters: vec![],
timezone: Timezone::None,
};
let mut depth = 1;
let mut components = Vec::with_capacity(3);
let mut property = None;
let mut parameter = None;
loop {
match stream.token()? {
Token::ElementStart { name, raw } => match name {
NamedElement {
ns: Namespace::Dav,
element: Element::Propname,
} if depth == 1 => {
cq.properties = PropFind::PropName;
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::Allprop,
} if depth == 1 => {
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
} if depth == 1 => {
cq.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
}
NamedElement {
ns: Namespace::CalDav,
element: Element::Filter,
} if depth == 1 => {
depth += 1;
}
NamedElement {
ns: Namespace::CalDav,
element: Element::Timezone,
} if depth == 1 => {
cq.timezone =
Timezone::Name(stream.collect_string_value()?.unwrap_or_default());
}
NamedElement {
ns: Namespace::CalDav,
element: Element::TimezoneId,
} if depth == 1 => {
cq.timezone =
Timezone::Id(stream.collect_string_value()?.unwrap_or_default());
}
NamedElement {
ns: Namespace::CalDav,
element: Element::CompFilter,
} if depth >= 2 => {
for attribute in raw.attributes::<ICalendarComponentType>() {
if let Attribute::Name(name) = attribute? {
components.push((name, depth));
}
}
depth += 1;
}
NamedElement {
ns: Namespace::CalDav,
element: Element::PropFilter,
} if depth >= 3 => {
for attribute in raw.attributes::<ICalendarProperty>() {
if let Attribute::Name(name) = attribute? {
property = Some(name);
}
}
depth += 1;
}
NamedElement {
ns: Namespace::CalDav,
element: Element::ParamFilter,
} if depth >= 4 => {
for attribute in raw.attributes::<ICalendarParameterName>() {
if let Attribute::Name(name) = attribute? {
parameter = Some(name);
}
}
depth += 1;
}
NamedElement {
ns: Namespace::CalDav,
element: Element::IsNotDefined,
} => {
stream.expect_element_end()?;
if let Some(filter) = Filter::from_parts(
components.iter().map(|(c, _)| c.clone()).collect(),
property.clone(),
parameter.clone(),
FilterOp::Undefined,
) {
cq.filters.push(filter);
}
}
NamedElement {
ns: Namespace::CalDav,
element: Element::TextMatch,
} => {
let mut tm = TextMatch::parse(raw)?;
tm.value = stream.collect_string_value()?.unwrap_or_default();
if let Some(filter) = Filter::from_parts(
components.iter().map(|(c, _)| c.clone()).collect(),
property.clone(),
parameter.clone(),
FilterOp::TextMatch(tm),
) {
cq.filters.push(filter);
}
}
NamedElement {
ns: Namespace::CalDav,
element: Element::TimeRange,
} => {
let range = TimeRange::from_raw(&raw)?;
stream.expect_element_end()?;
if let Some(filter) = range.and_then(|range| {
Filter::from_parts(
components.iter().map(|(c, _)| c.clone()).collect(),
property.clone(),
parameter.clone(),
FilterOp::TimeRange(range),
)
}) {
cq.filters.push(filter);
}
}
name => return Err(name.into_unexpected()),
},
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
if matches!(components.last(), Some((_, d)) if *d == depth) {
if components.len() > 1
&& cq
.filters
.last()
.and_then(|c| c.components())
.is_none_or(|c| c.len() < components.len())
{
cq.filters.push(Filter::Component {
comp: components.iter().map(|(c, _)| c.clone()).collect(),
op: FilterOp::Exists,
});
}
components.pop();
}
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
element => return Err(element.into_unexpected()),
}
}
Ok(cq)
}
}
impl DavParser for AddressbookQuery {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut aq = AddressbookQuery {
properties: PropFind::AllProp(vec![]),
filters: vec![],
limit: None,
};
let mut depth = 1;
let mut property = None;
let mut parameter = None;
loop {
match stream.token()? {
Token::ElementStart { name, raw } => match name {
NamedElement {
ns: Namespace::Dav,
element: Element::Propname,
} if depth == 1 => {
aq.properties = PropFind::PropName;
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::Allprop,
} if depth == 1 => {
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
} if depth == 1 => {
aq.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
}
NamedElement {
ns: Namespace::CardDav,
element: Element::Filter,
} if depth == 1 => {
if let Some(filter) = Filter::parse(raw)? {
aq.filters.push(filter);
}
depth += 1;
}
NamedElement {
ns: Namespace::CardDav,
element: Element::Limit,
} if depth == 1 => {
stream.expect_named_element(NamedElement::carddav(Element::Nresults))?;
if let Some(Ok(limit)) = stream.parse_value::<u32>()? {
aq.limit = limit.into();
}
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::CardDav,
element: Element::PropFilter,
} if depth == 2 => {
let mut filter = None;
for attribute in raw.attributes::<VCardPropertyWithGroup>() {
match attribute? {
Attribute::Name(name) => {
property = Some(name);
}
Attribute::TestAllOf(all_of) => {
filter =
(if all_of { Filter::AllOf } else { Filter::AnyOf }).into();
}
_ => {}
}
}
if let Some(filter) = filter {
aq.filters.push(filter);
}
depth += 1;
}
NamedElement {
ns: Namespace::CardDav,
element: Element::ParamFilter,
} if depth == 3 => {
for attribute in raw.attributes::<VCardParameterName>() {
if let Attribute::Name(name) = attribute? {
parameter = Some(name);
}
}
depth += 1;
}
NamedElement {
ns: Namespace::CardDav,
element: Element::IsNotDefined,
} => {
stream.expect_element_end()?;
if let Some(filter) = Filter::from_parts(
(),
property.clone(),
parameter.clone(),
FilterOp::Undefined,
) {
aq.filters.push(filter);
}
}
NamedElement {
ns: Namespace::CardDav,
element: Element::TextMatch,
} => {
let mut tm = TextMatch::parse(raw)?;
tm.value = stream.collect_string_value()?.unwrap_or_default();
if let Some(filter) = Filter::from_parts(
(),
property.clone(),
parameter.clone(),
FilterOp::TextMatch(tm),
) {
aq.filters.push(filter);
}
}
name => return Err(name.into_unexpected()),
},
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
element => return Err(element.into_unexpected()),
}
}
Ok(aq)
}
}
impl DavParser for FreeBusyQuery {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
match stream.token()? {
Token::ElementStart {
name:
NamedElement {
ns: Namespace::CalDav,
element: Element::TimeRange,
},
raw,
} => TimeRange::from_raw(&raw).map(|range| FreeBusyQuery { range }),
other => Err(other.into_unexpected()),
}
}
}
impl DavParser for MultiGet {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut mg = MultiGet {
properties: PropFind::AllProp(vec![]),
hrefs: vec![],
};
loop {
match stream.token()? {
Token::ElementStart { name, .. } => match name {
NamedElement {
ns: Namespace::Dav,
element: Element::Propname,
} => {
mg.properties = PropFind::PropName;
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::Allprop,
} => {
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
} => {
mg.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
}
NamedElement {
ns: Namespace::Dav,
element: Element::Href,
} => {
if let Some(href) = stream.collect_string_value()? {
mg.hrefs.push(href);
}
}
name => return Err(name.into_unexpected()),
},
Token::ElementEnd => {
break;
}
element => return Err(element.into_unexpected()),
}
}
Ok(mg)
}
}
impl DavParser for SyncCollection {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut sc = SyncCollection {
properties: PropFind::AllProp(vec![]),
limit: None,
sync_token: None,
depth: Depth::None,
};
loop {
match stream.token()? {
Token::ElementStart { name, .. } => match name {
NamedElement {
ns: Namespace::Dav,
element: Element::Prop,
} => {
sc.properties = PropFind::Prop(stream.collect_properties(Vec::new())?);
}
NamedElement {
ns: Namespace::Dav,
element: Element::Limit,
} => {
stream.expect_named_element(NamedElement::dav(Element::Nresults))?;
if let Some(Ok(limit)) = stream.parse_value::<u32>()? {
sc.limit = limit.into();
}
stream.expect_element_end()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::SyncToken,
} => {
sc.sync_token = stream.collect_string_value()?;
}
NamedElement {
ns: Namespace::Dav,
element: Element::SyncLevel,
} => {
if let Some(Ok(depth)) = stream.parse_value::<Depth>()? {
sc.depth = depth;
}
}
name => return Err(name.into_unexpected()),
},
Token::ElementEnd => {
break;
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
element => return Err(element.into_unexpected()),
}
}
Ok(sc)
}
}
impl DavParser for ExpandProperty {
fn parse(stream: &mut Tokenizer<'_>) -> crate::parser::Result<Self> {
let mut ep = ExpandProperty { properties: vec![] };
let mut depth = 1;
loop {
match stream.token()? {
Token::ElementStart { name, raw } => match name {
NamedElement {
ns,
element: Element::Property,
} => {
for attribute in raw.attributes::<String>() {
if let Attribute::Name(name) = attribute? {
if let Some(property) = Element::try_parse(name.as_bytes())
.copied()
.and_then(|element| {
DavProperty::from_element(NamedElement { ns, element })
})
{
ep.properties.push(ExpandPropertyItem {
property,
depth: depth - 1,
});
} else {
let attrs = raw.element.attributes_raw().trim_ascii();
ep.properties.push(ExpandPropertyItem {
property: DavProperty::DeadProperty(DeadElementTag {
name,
attrs: (!attrs.is_empty()).then(|| {
String::from_utf8_lossy(attrs).into_owned()
}),
}),
depth: depth - 1,
});
}
break;
}
}
depth += 1;
}
name => return Err(name.into_unexpected()),
},
Token::ElementEnd => {
depth -= 1;
if depth == 0 {
break;
}
}
Token::UnknownElement(_) => {
stream.seek_element_end()?;
}
element => return Err(element.into_unexpected()),
}
}
Ok(ep)
}
}
impl TextMatch {
fn parse(raw: RawElement<'_>) -> crate::parser::Result<Self> {
let mut tm = TextMatch {
match_type: MatchType::Contains,
value: String::new(),
collation: Collation::AsciiCasemap,
negate: false,
};
for attribute in raw.attributes::<String>() {
match attribute? {
Attribute::MatchType(match_type) => {
tm.match_type = match_type;
}
Attribute::NegateCondition(negate) => {
tm.negate = negate;
}
Attribute::Collation(collation) => {
tm.collation = collation;
}
_ => {}
}
}
Ok(tm)
}
}
impl<A, B, C> Filter<A, B, C> {
fn from_parts(comp: A, prop: Option<B>, param: Option<C>, op: FilterOp) -> Option<Self> {
match (prop, param) {
(Some(prop), Some(param)) => Some(Filter::Parameter {
comp,
prop,
param,
op,
}),
(Some(prop), None) => Some(Filter::Property { comp, prop, op }),
(None, None) => Some(Filter::Component { comp, op }),
_ => None,
}
}
fn components(&self) -> Option<&A> {
match self {
Filter::Component { comp, .. } => Some(comp),
Filter::Property { comp, .. } => Some(comp),
Filter::Parameter { comp, .. } => Some(comp),
_ => None,
}
}
fn parse(raw: RawElement<'_>) -> crate::parser::Result<Option<Self>> {
for attribute in raw.attributes::<String>() {
if let Attribute::TestAllOf(all_of) = attribute? {
return Ok(Some(if all_of { Filter::AllOf } else { Filter::AnyOf }));
}
}
Ok(None)
}
}
impl XmlValueParser for Depth {
fn parse_bytes(bytes: &[u8]) -> Option<Self> {
Depth::parse(bytes)
}
fn parse_str(text: &str) -> Option<Self> {
Depth::parse(text.as_bytes())
}
}
+426
View File
@@ -0,0 +1,426 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
responses::XmlEscape,
schema::{
Namespace, Namespaces,
property::{DavProperty, Privilege},
response::{
Ace, AclRestrictions, GrantDeny, Href, List, Principal, PrincipalSearchProperty,
PrincipalSearchPropertySet, RequiredPrincipal, Resource, SupportedPrivilege,
},
},
};
use std::fmt::Display;
impl Display for SupportedPrivilege {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:supported-privilege>{}", self.privilege)?;
if self.abstract_ {
write!(f, "<D:abstract/>")?;
}
write!(f, "<D:description>")?;
self.description.write_escaped_to(f)?;
write!(
f,
"</D:description>{}</D:supported-privilege>",
self.supported_privilege
)
}
}
impl SupportedPrivilege {
pub fn new(privilege: Privilege, description: impl Into<String>) -> Self {
SupportedPrivilege {
privilege,
abstract_: false,
description: description.into(),
supported_privilege: List(vec![]),
}
}
pub fn with_abstract(mut self) -> Self {
self.abstract_ = true;
self
}
pub fn with_supported_privilege(mut self, supported_privilege: SupportedPrivilege) -> Self {
self.supported_privilege.0.push(supported_privilege);
self
}
pub fn with_opt_supported_privilege(
mut self,
supported_privilege: Option<SupportedPrivilege>,
) -> Self {
if let Some(supported_privilege) = supported_privilege {
self.supported_privilege.0.push(supported_privilege);
}
self
}
pub fn all_privileges(is_calendar: bool) -> SupportedPrivilege {
SupportedPrivilege::new(Privilege::All, "Any operation")
.with_abstract()
.with_supported_privilege(
SupportedPrivilege::new(Privilege::Read, "Read objects").with_supported_privilege(
SupportedPrivilege::new(
Privilege::ReadCurrentUserPrivilegeSet,
"Read current user privileges",
),
),
)
.with_supported_privilege(
SupportedPrivilege::new(Privilege::Write, "Write objects")
.with_supported_privilege(SupportedPrivilege::new(
Privilege::WriteProperties,
"Write properties",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::WriteContent,
"Write object contents",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::Bind,
"Add resources to a collection",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::Unbind,
"Remove resources from a collection",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::Unlock,
"Unlock resources",
)),
)
.with_supported_privilege(SupportedPrivilege::new(Privilege::ReadAcl, "Read ACL"))
.with_supported_privilege(SupportedPrivilege::new(Privilege::WriteAcl, "Write ACL"))
.with_opt_supported_privilege((is_calendar).then(|| {
SupportedPrivilege::new(Privilege::ReadFreeBusy, "Read free/busy information")
}))
}
pub fn all_scheduling_privileges(is_inbox: bool) -> SupportedPrivilege {
let privilege = SupportedPrivilege::new(Privilege::All, "Any operation")
.with_abstract()
.with_supported_privilege(
SupportedPrivilege::new(Privilege::Read, "Read objects").with_supported_privilege(
SupportedPrivilege::new(
Privilege::ReadCurrentUserPrivilegeSet,
"Read current user privileges",
),
),
);
if is_inbox {
privilege.with_supported_privilege(
SupportedPrivilege::new(
Privilege::ScheduleDeliver,
"Deliver calendar scheduling messages",
)
.with_supported_privilege(SupportedPrivilege::new(
Privilege::ScheduleDeliverInvite,
"Deliver calendar scheduling invites",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::ScheduleDeliverReply,
"Deliver calendar scheduling replies",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::ScheduleQueryFreeBusy,
"Query free/busy information",
)),
)
} else {
privilege.with_supported_privilege(
SupportedPrivilege::new(
Privilege::ScheduleSend,
"Send calendar scheduling messages",
)
.with_supported_privilege(SupportedPrivilege::new(
Privilege::ScheduleSendInvite,
"Send calendar scheduling invites",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::ScheduleSendReply,
"Send calendar scheduling replies",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::ScheduleSendFreeBusy,
"Send free/busy information",
)),
)
}
}
}
impl Display for Ace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:ace>")?;
if self.invert {
write!(f, "<D:invert>")?;
}
self.principal.fmt(f)?;
if self.invert {
write!(f, "</D:invert>")?;
}
self.grant_deny.fmt(f)?;
if self.protected {
write!(f, "<D:protected/>")?;
}
if let Some(inherited) = &self.inherited {
write!(f, "<D:inherited>")?;
inherited.fmt(f)?;
write!(f, "</D:inherited>")?;
}
write!(f, "</D:ace>")
}
}
impl Display for Principal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:principal>")?;
match self {
Principal::Href(href) => href.fmt(f),
Principal::Response(response) => response.fmt(f),
Principal::All => "<D:all/>".fmt(f),
Principal::Authenticated => "<D:authenticated/>".fmt(f),
Principal::Unauthenticated => "<D:unauthenticated/>".fmt(f),
Principal::Property(property) => {
write!(f, "<D:property>{}</D:property>", property)
}
Principal::Self_ => "<D:self/>".fmt(f),
}?;
write!(f, "</D:principal>")
}
}
impl Display for GrantDeny {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GrantDeny::Grant(privileges) => {
write!(f, "<D:grant>")?;
privileges.fmt(f)?;
write!(f, "</D:grant>")
}
GrantDeny::Deny(privileges) => {
write!(f, "<D:deny>")?;
privileges.fmt(f)?;
write!(f, "</D:deny>")
}
}
}
}
impl Display for AclRestrictions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.grant_only {
write!(f, "<D:grant-only/>")?;
}
if self.no_invert {
write!(f, "<D:no-invert/>")?;
}
if self.deny_before_grant {
write!(f, "<D:deny-before-grant/>")?;
}
if let Some(required_principal) = &self.required_principal {
required_principal.fmt(f)?;
}
Ok(())
}
}
impl Display for RequiredPrincipal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:required-principal>")?;
match self {
RequiredPrincipal::All => "<D:all/>".fmt(f)?,
RequiredPrincipal::Authenticated => "<D:authenticated/>".fmt(f)?,
RequiredPrincipal::Unauthenticated => "<D:unauthenticated/>".fmt(f)?,
RequiredPrincipal::Self_ => "<D:self/>".fmt(f)?,
RequiredPrincipal::Href(hrefs) => hrefs.fmt(f)?,
RequiredPrincipal::Property(properties) => {
for property in properties {
write!(f, "<D:property>{}</D:property>", property)?;
}
}
}
write!(f, "</D:required-principal>")
}
}
impl Display for Privilege {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Privilege::Read => "<D:privilege><D:read/></D:privilege>".fmt(f),
Privilege::Write => "<D:privilege><D:write/></D:privilege>".fmt(f),
Privilege::WriteProperties => "<D:privilege><D:write-properties/></D:privilege>".fmt(f),
Privilege::WriteContent => "<D:privilege><D:write-content/></D:privilege>".fmt(f),
Privilege::Unlock => "<D:privilege><D:unlock/></D:privilege>".fmt(f),
Privilege::ReadAcl => "<D:privilege><D:read-acl/></D:privilege>".fmt(f),
Privilege::ReadCurrentUserPrivilegeSet => {
"<D:privilege><D:read-current-user-privilege-set/></D:privilege>".fmt(f)
}
Privilege::WriteAcl => "<D:privilege><D:write-acl/></D:privilege>".fmt(f),
Privilege::Bind => "<D:privilege><D:bind/></D:privilege>".fmt(f),
Privilege::Unbind => "<D:privilege><D:unbind/></D:privilege>".fmt(f),
Privilege::All => "<D:privilege><D:all/></D:privilege>".fmt(f),
Privilege::ReadFreeBusy => "<D:privilege><A:read-free-busy/></D:privilege>".fmt(f),
Privilege::ScheduleDeliver => "<D:privilege><A:schedule-deliver/></D:privilege>".fmt(f),
Privilege::ScheduleDeliverInvite => {
"<D:privilege><A:schedule-deliver-invite/></D:privilege>".fmt(f)
}
Privilege::ScheduleDeliverReply => {
"<D:privilege><A:schedule-deliver-reply/></D:privilege>".fmt(f)
}
Privilege::ScheduleQueryFreeBusy => {
"<D:privilege><A:schedule-query-freebusy/></D:privilege>".fmt(f)
}
Privilege::ScheduleSend => "<D:privilege><A:schedule-send/></D:privilege>".fmt(f),
Privilege::ScheduleSendInvite => {
"<D:privilege><A:schedule-send-invite/></D:privilege>".fmt(f)
}
Privilege::ScheduleSendReply => {
"<D:privilege><A:schedule-send-reply/></D:privilege>".fmt(f)
}
Privilege::ScheduleSendFreeBusy => {
"<D:privilege><A:schedule-send-freebusy/></D:privilege>".fmt(f)
}
}
}
}
impl Display for Resource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<D:resource>{}{}</D:resource>",
self.href, self.privilege
)
}
}
impl Display for PrincipalSearchPropertySet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
write!(
f,
"<D:principal-search-property-set {}>{}</D:principal-search-property-set>",
self.namespaces, self.properties
)
}
}
impl Display for PrincipalSearchProperty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<D:principal-search-property><D:prop>{}</D:prop>",
self.name
)?;
write!(
f,
"<D:description>{}</D:description></D:principal-search-property>",
self.description
)
}
}
impl Resource {
pub fn new(href: impl Into<String>, privilege: Privilege) -> Self {
Resource {
href: Href(href.into()),
privilege,
}
}
}
impl PrincipalSearchPropertySet {
pub fn new(properties: Vec<PrincipalSearchProperty>) -> Self {
PrincipalSearchPropertySet {
namespaces: Namespaces::default(),
properties: List(properties),
}
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespaces.set(namespace);
self
}
}
impl PrincipalSearchProperty {
pub fn new(name: impl Into<DavProperty>, description: impl Into<String>) -> Self {
PrincipalSearchProperty {
name: name.into(),
description: description.into(),
}
}
}
impl Ace {
pub fn new(principal: Principal, grant_deny: GrantDeny) -> Self {
Ace {
principal,
invert: false,
grant_deny,
protected: false,
inherited: None,
}
}
pub fn with_invert(mut self) -> Self {
self.invert = true;
self
}
pub fn with_protected(mut self) -> Self {
self.protected = true;
self
}
pub fn with_inherited(mut self, inherited: impl Into<String>) -> Self {
self.inherited = Some(Href(inherited.into()));
self
}
}
impl GrantDeny {
pub fn grant(privileges: Vec<Privilege>) -> Self {
GrantDeny::Grant(List(privileges))
}
pub fn deny(privileges: Vec<Privilege>) -> Self {
GrantDeny::Deny(List(privileges))
}
}
impl AclRestrictions {
pub fn new() -> Self {
Self::default()
}
pub fn with_grant_only(mut self) -> Self {
self.grant_only = true;
self
}
pub fn with_no_invert(mut self) -> Self {
self.no_invert = true;
self
}
pub fn with_deny_before_grant(mut self) -> Self {
self.deny_before_grant = true;
self
}
pub fn with_required_principal(mut self, required_principal: RequiredPrincipal) -> Self {
self.required_principal = Some(required_principal);
self
}
}
+201
View File
@@ -0,0 +1,201 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::schema::{
Namespace, Namespaces,
response::{BaseCondition, CalCondition, CardCondition, Condition, ErrorResponse},
};
use std::fmt::Display;
impl Display for ErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><D:error {}>",
self.namespaces
)?;
match &self.error {
Condition::Base(e) => e.fmt(f)?,
Condition::Cal(e) => e.fmt(f)?,
Condition::Card(e) => e.fmt(f)?,
}
write!(f, "</D:error>")
}
}
impl Display for Condition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:error>")?;
match self {
Condition::Base(e) => e.fmt(f)?,
Condition::Cal(e) => e.fmt(f)?,
Condition::Card(e) => e.fmt(f)?,
}
write!(f, "</D:error>")
}
}
impl Display for BaseCondition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BaseCondition::NoConflictingLock(items) => {
write!(f, "<D:no-conflicting-lock>{items}</D:no-conflicting-lock>")
}
BaseCondition::LockTokenSubmitted(items) => write!(
f,
"<D:lock-token-submitted>{items}</D:lock-token-submitted>"
),
BaseCondition::LockTokenMatchesRequestUri => {
write!(f, "<D:lock-token-matches-request-uri/>")
}
BaseCondition::CannotModifyProtectedProperty => {
write!(f, "<D:cannot-modify-protected-property/>")
}
BaseCondition::NoExternalEntities => write!(f, "<D:no-external-entities/>"),
BaseCondition::PreservedLiveProperties => write!(f, "<D:preserved-live-properties/>"),
BaseCondition::PropFindFiniteDepth => write!(f, "<D:propfind-finite-depth/>"),
BaseCondition::ResourceMustBeNull => write!(f, "<D:resource-must-be-null/>"),
BaseCondition::NeedPrivileges(resources) => {
write!(f, "<D:need-privileges>{resources}</D:need-privileges>")
}
BaseCondition::NumberOfMatchesWithinLimit => {
write!(f, "<D:number-of-matches-within-limits/>")
}
BaseCondition::QuotaNotExceeded => write!(f, "<D:quota-not-exceeded/>"),
BaseCondition::ValidResourceType => write!(f, "<D:valid-resourcetype/>"),
BaseCondition::ValidSyncToken => write!(f, "<D:valid-sync-token/>"),
BaseCondition::NoAceConflict => write!(f, "<D:no-ace-conflict/>"),
BaseCondition::NoProtectedAceConflict => write!(f, "<D:no-protected-ace-conflict/>"),
BaseCondition::NoInheritedAceConflict => write!(f, "<D:no-inherited-ace-conflict/>"),
BaseCondition::LimitedNumberOfAces => write!(f, "<D:limited-number-of-aces/>"),
BaseCondition::DenyBeforeGrant => write!(f, "<D:deny-before-grant/>"),
BaseCondition::GrantOnly => write!(f, "<D:grant-only/>"),
BaseCondition::NoInvert => write!(f, "<D:no-invert/>"),
BaseCondition::NoAbstract => write!(f, "<D:no-abstract/>"),
BaseCondition::NotSupportedPrivilege => write!(f, "<D:not-supported-privilege/>"),
BaseCondition::MissingRequiredPrincipal => write!(f, "<D:missing-required-principal/>"),
BaseCondition::RecognizedPrincipal => write!(f, "<D:recognized-principal/>"),
BaseCondition::AllowedPrincipal => write!(f, "<D:allowed-principal/>"),
}
}
}
impl Display for CalCondition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CalCondition::CalendarCollectionLocationOk => {
write!(f, "<A:calendar-collection-location-ok/>")
}
CalCondition::ValidCalendarData => write!(f, "<A:valid-calendar-data/>"),
CalCondition::ValidFilter => write!(f, "<A:valid-filter/>"),
CalCondition::ValidTimezone => write!(f, "<A:valid-timezone/>"),
CalCondition::ValidCalendarObjectResource => {
write!(f, "<A:valid-calendar-object-resource/>")
}
CalCondition::NoUidConflict(uid) => {
write!(f, "<A:no-uid-conflict>{uid}</A:no-uid-conflict>")
}
CalCondition::InitializeCalendarCollection => {
write!(f, "<A:initialize-calendar-collection/>")
}
CalCondition::SupportedCalendarData => write!(f, "<A:supported-calendar-data/>"),
CalCondition::SupportedFilter(_) => write!(f, "<A:supported-filter/>"),
CalCondition::SupportedCollation(c) => {
write!(f, "<A:supported-collation>{c}</A:supported-collation>")
}
CalCondition::MinDateTime => write!(f, "<A:min-date-time/>"),
CalCondition::MaxDateTime => write!(f, "<A:max-date-time/>"),
CalCondition::MaxResourceSize(l) => {
write!(f, "<A:max-resource-size>{l}</A:max-resource-size>")
}
CalCondition::MaxInstances => write!(f, "<A:max-instances/>"),
CalCondition::MaxAttendeesPerInstance => write!(f, "<A:max-attendees-per-instance/>"),
CalCondition::UniqueSchedulingObjectResource(href) => write!(
f,
"<A:unique-scheduling-object-resource>{href}</A:unique-scheduling-object-resource>"
),
CalCondition::SameOrganizerInAllComponents => {
write!(f, "<A:same-organizer-in-all-components/>")
}
CalCondition::AllowedOrganizerObjectChange => {
write!(f, "<A:allowed-organizer-scheduling-object-change/>")
}
CalCondition::AllowedAttendeeObjectChange => {
write!(f, "<A:allowed-attendee-scheduling-object-change/>")
}
CalCondition::DefaultCalendarNeeded => write!(f, "<A:default-calendar-needed/>"),
CalCondition::ValidScheduleDefaultCalendarUrl => {
write!(f, "<A:valid-schedule-default-calendar-URL/>")
}
CalCondition::ValidSchedulingMessage => write!(f, "<A:valid-scheduling-message/>"),
CalCondition::ValidOrganizer => write!(f, "<A:valid-organizer/>"),
CalCondition::SupportedCalendarComponent => {
write!(f, "<A:supported-calendar-component/>")
}
}
}
}
impl Display for CardCondition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CardCondition::SupportedAddressData => write!(f, "<B:supported-address-data/>"),
CardCondition::SupportedAddressDataConversion => {
write!(f, "<B:supported-address-data-conversion/>")
}
CardCondition::SupportedFilter(_) => write!(f, "<B:supported-filter/>"),
CardCondition::SupportedCollation(c) => {
write!(f, "<B:supported-collation>{c}</B:supported-collation>")
}
CardCondition::ValidAddressData => write!(f, "<B:valid-address-data/>"),
CardCondition::NoUidConflict(uid) => {
write!(f, "<B:no-uid-conflict>{uid}</B:no-uid-conflict>")
}
CardCondition::MaxResourceSize(l) => {
write!(f, "<B:max-resource-size>{l}</B:max-resource-size>")
}
CardCondition::AddressBookCollectionLocationOk => {
write!(f, "<B:addressbook-collection-location-ok/>")
}
}
}
}
impl From<CalCondition> for Condition {
fn from(error: CalCondition) -> Self {
Condition::Cal(error)
}
}
impl From<CardCondition> for Condition {
fn from(error: CardCondition) -> Self {
Condition::Card(error)
}
}
impl From<BaseCondition> for Condition {
fn from(error: BaseCondition) -> Self {
Condition::Base(error)
}
}
impl ErrorResponse {
pub fn new(error: impl Into<Condition>) -> Self {
ErrorResponse {
namespaces: Namespaces::default(),
error: error.into(),
}
}
pub fn with_namespace(mut self, namespace: impl Into<Namespace>) -> Self {
self.namespaces.set(namespace.into());
self
}
}
+174
View File
@@ -0,0 +1,174 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Depth, Timeout,
responses::DeadPropertyFormat,
schema::{
property::{ActiveLock, LockDiscovery, LockEntry, LockScope, LockType, SupportedLock},
request::LockInfo,
response::{Href, List},
},
};
use std::fmt::Display;
use types::dead_property::DeadProperty;
impl Display for SupportedLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:supportedlock>{}</D:supportedlock>", self.0)
}
}
impl Display for LockDiscovery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:lockdiscovery>{}</D:lockdiscovery>", self.0)
}
}
impl Display for ActiveLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<D:activelock>{}{}{}",
self.lock_scope, self.lock_type, self.depth
)?;
if let Some(owner) = &self.owner {
f.write_str("<D:owner>")?;
owner.fmt(f)?;
f.write_str("</D:owner>")?;
}
write!(f, "{}", self.timeout)?;
if let Some(lock_token) = &self.lock_token {
write!(f, "<D:locktoken>{}</D:locktoken>", lock_token)?;
}
write!(
f,
"<D:lockroot>{}</D:lockroot></D:activelock>",
self.lock_root
)
}
}
impl Display for Depth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Depth::Zero => write!(f, "<D:depth>0</D:depth>"),
Depth::One => write!(f, "<D:depth>1</D:depth>"),
Depth::Infinity => write!(f, "<D:depth>infinity</D:depth>"),
Depth::None => write!(f, "<D:depth/>"),
}
}
}
impl Display for Timeout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Timeout::Infinite => write!(f, "<D:timeout>Infinite</D:timeout>"),
Timeout::Second(s) => write!(f, "<D:timeout>Second-{}</D:timeout>", s),
Timeout::None => Ok(()),
}
}
}
impl Display for LockInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:lockinfo>{}{}", self.lock_scope, self.lock_type)?;
if let Some(owner) = &self.owner {
f.write_str("<D:owner>")?;
owner.fmt(f)?;
f.write_str("</D:owner>")?;
}
write!(f, "</D:lockinfo>",)
}
}
impl Display for LockEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<D:lockentry>{}{}</D:lockentry>",
self.lock_scope, self.lock_type
)
}
}
impl Display for LockScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockScope::Exclusive => write!(f, "<D:lockscope><D:exclusive/></D:lockscope>"),
LockScope::Shared => write!(f, "<D:lockscope><D:shared/></D:lockscope>"),
}
}
}
impl Display for LockType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockType::Write => write!(f, "<D:locktype><D:write/></D:locktype>"),
LockType::Other => write!(f, "<D:locktype><D:other/></D:locktype>"),
}
}
}
impl ActiveLock {
pub fn new(href: impl Into<String>, lock_scope: LockScope) -> Self {
Self {
lock_scope,
lock_type: LockType::Write,
depth: Depth::Infinity,
owner: None,
timeout: Timeout::Infinite,
lock_token: None,
lock_root: Href(href.into()),
}
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_timeout(mut self, timeout: u64) -> Self {
self.timeout = Timeout::Second(timeout);
self
}
pub fn with_owner_opt(mut self, owner: Option<DeadProperty>) -> Self {
self.owner = owner;
self
}
pub fn with_owner(mut self, owner: DeadProperty) -> Self {
self.owner = Some(owner);
self
}
pub fn with_lock_token(mut self, token: impl Into<String>) -> Self {
self.lock_token = Some(Href(token.into()));
self
}
}
impl Default for SupportedLock {
fn default() -> Self {
Self(List(vec![
LockEntry {
lock_scope: LockScope::Exclusive,
lock_type: LockType::Write,
},
LockEntry {
lock_scope: LockScope::Shared,
lock_type: LockType::Write,
},
]))
}
}
+53
View File
@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::schema::{
Namespace, Namespaces,
response::{List, MkColResponse, PropStat},
};
use std::fmt::Display;
impl Display for MkColResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
if !self.mkcalendar {
write!(
f,
"<D:mkcol-response {}>{}</D:mkcol-response>",
self.namespaces, self.propstat
)
} else {
write!(
f,
"<A:mkcalendar-response {}>{}</A:mkcalendar-response>",
self.namespaces, self.propstat
)
}
}
}
impl MkColResponse {
pub fn new(propstat: Vec<PropStat>) -> Self {
Self {
namespaces: Namespaces::default(),
propstat: List(propstat),
mkcalendar: false,
}
}
pub fn with_mkcalendar(mut self, mkcalendar: bool) -> Self {
self.mkcalendar = mkcalendar;
if mkcalendar {
self.namespaces.set(Namespace::CalDav);
}
self
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespaces.set(namespace);
self
}
}
+750
View File
@@ -0,0 +1,750 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod acl;
pub mod error;
pub mod lock;
pub mod mkcol;
pub mod multistatus;
pub mod property;
pub mod propstat;
pub mod schedule;
use crate::schema::{
Namespaces,
property::{Comp, ResourceType, SupportedCollation},
response::{Href, List, Location, ResponseDescription, Status, SyncToken},
};
use std::fmt::{Display, Write};
use types::dead_property::{DeadProperty, DeadPropertyTag};
trait XmlEscape {
fn write_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result;
}
trait XmlCdataEscape {
fn write_cdata_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result;
}
impl<T: AsRef<str>> XmlEscape for T {
fn write_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result {
let str = self.as_ref();
for c in str.chars() {
match c {
'<' => out.write_str("&lt;")?,
'>' => out.write_str("&gt;")?,
'&' => out.write_str("&amp;")?,
'"' => out.write_str("&quot;")?,
'\'' => out.write_str("&apos;")?,
_ => out.write_char(c)?,
}
}
Ok(())
}
}
impl<T: AsRef<str>> XmlCdataEscape for T {
fn write_cdata_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result {
let str = self.as_ref();
let mut last_ch = '\0';
let mut last_ch2 = '\0';
out.write_str("<![CDATA[")?;
for ch in str.chars() {
match ch {
'>' if last_ch == ']' && last_ch2 == ']' => {
out.write_str("]]><![CDATA[>")?;
}
_ => out.write_char(ch)?,
}
last_ch2 = last_ch;
last_ch = ch;
}
out.write_str("]]>")
}
}
impl Display for Namespaces {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("xmlns:D=\"DAV:\"")?;
if self.cal {
f.write_str(" xmlns:A=\"urn:ietf:params:xml:ns:caldav\"")?;
}
if self.card {
f.write_str(" xmlns:B=\"urn:ietf:params:xml:ns:carddav\"")?;
}
if self.cs {
f.write_str(" xmlns:C=\"http://calendarserver.org/ns/\"")?;
}
Ok(())
}
}
impl Display for Href {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:href>")?;
self.0.write_escaped_to(f)?;
write!(f, "</D:href>")
}
}
impl<T: Display> Display for List<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for item in &self.0 {
item.fmt(f)?;
}
Ok(())
}
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:status>")?;
write!(f, "HTTP/1.1 {}", self.0)?;
write!(f, "</D:status>")
}
}
impl Display for ResponseDescription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:responsedescription>")?;
self.0.write_escaped_to(f)?;
write!(f, "</D:responsedescription>")
}
}
impl Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:location>")?;
self.0.fmt(f)?;
write!(f, "</D:location>")
}
}
impl Display for SyncToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:sync-token>")?;
self.0.write_escaped_to(f)?;
write!(f, "</D:sync-token>")
}
}
impl Display for Comp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<A:comp name=\"{}\"/>", self.0.as_str())
}
}
impl Display for ResourceType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResourceType::Collection => write!(f, "<D:collection/>"),
ResourceType::Principal => write!(f, "<D:principal/>"),
ResourceType::AddressBook => write!(f, "<B:addressbook/>"),
ResourceType::Calendar => write!(f, "<A:calendar/>"),
ResourceType::ScheduleInbox => write!(f, "<A:schedule-inbox/>"),
ResourceType::ScheduleOutbox => write!(f, "<A:schedule-outbox/>"),
}
}
}
impl Display for SupportedCollation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ns = self.namespace.prefix();
write!(
f,
"<{ns}:supported-collation>{}</{ns}:supported-collation>",
self.collation.as_str()
)
}
}
pub trait DeadPropertyFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
impl DeadPropertyFormat for DeadProperty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut last_tag = "";
for item in &self.0 {
match item {
DeadPropertyTag::ElementStart(tag) => {
let name = &tag.name;
if let Some(attrs) = &tag.attrs {
write!(f, "<{name} {attrs}>")?;
} else {
write!(f, "<{name}>")?;
}
last_tag = name;
}
DeadPropertyTag::ElementEnd => {
write!(f, "</{}>", last_tag)?;
}
DeadPropertyTag::Text(text) => {
text.write_escaped_to(f)?;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::fmt::Display;
use calcard::{icalendar::ICalendar, vcard::VCard};
use hyper::StatusCode;
use mail_parser::DateTime;
use types::dead_property::{DeadElementTag, DeadProperty, DeadPropertyTag};
use crate::{
Depth,
parser::{Token, tokenizer::Tokenizer},
responses::XmlCdataEscape,
schema::{
Namespace,
property::{
ActiveLock, CalDavProperty, CardDavProperty, DavValue, LockScope, Privilege,
ResourceType, Rfc1123DateTime, SupportedLock, WebDavProperty,
},
request::DavPropertyValue,
response::{
Ace, AclRestrictions, BaseCondition, ErrorResponse, GrantDeny, Href, List,
MkColResponse, MultiStatus, Principal, PrincipalSearchProperty,
PrincipalSearchPropertySet, PropResponse, PropStat, RequiredPrincipal, Resource,
Response, ScheduleResponse, ScheduleResponseItem, SupportedPrivilege,
},
},
};
impl<T: Display> List<T> {
pub fn new(vec: impl IntoIterator<Item = T>) -> Self {
List(vec.into_iter().collect())
}
}
impl From<ICalendar> for DavValue {
fn from(v: ICalendar) -> Self {
DavValue::ICalendar(v)
}
}
impl From<VCard> for DavValue {
fn from(v: VCard) -> Self {
DavValue::VCard(v)
}
}
#[test]
fn parse_responses() {
for (num, test) in [
// 001.xml
ErrorResponse::new(BaseCondition::LockTokenSubmitted(List::new([Href(
"/locked/".to_string(),
)])))
.to_string(),
// 002.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/file",
vec![
PropStat::new(DavPropertyValue::new(
WebDavProperty::DisplayName,
"Box type A",
)),
PropStat::new(DavPropertyValue::new(
WebDavProperty::DisplayName,
"Box type B",
))
.with_status(StatusCode::FORBIDDEN)
.with_response_description(
"The user does not have access to the DingALing property.",
),
],
)])
.with_response_description("There has been an access violation error.")
.to_string(),
// 003.xml
MultiStatus::new(vec![
Response::new_propstat(
"/container/",
vec![PropStat::new_list(vec![
DavPropertyValue::new(
WebDavProperty::CreationDate,
DateTime::parse_rfc3339("1997-12-01T17:42:21-08:00Z").unwrap(),
),
DavPropertyValue::new(WebDavProperty::DisplayName, "Example collection"),
DavPropertyValue::new(
WebDavProperty::ResourceType,
vec![ResourceType::Collection],
),
DavPropertyValue::new(
WebDavProperty::SupportedLock,
SupportedLock::default(),
),
])],
),
Response::new_propstat(
"/container/front.html",
vec![PropStat::new_list(vec![
DavPropertyValue::new(
WebDavProperty::CreationDate,
DateTime::parse_rfc3339("1997-12-01T18:27:21-08:00").unwrap(),
),
DavPropertyValue::new(WebDavProperty::DisplayName, "Example HTML resource"),
DavPropertyValue::new(WebDavProperty::GetContentLength, 4525u64),
DavPropertyValue::new(WebDavProperty::GetContentType, "text/html"),
DavPropertyValue::new(WebDavProperty::GetETag, "\"zzyzx\""),
DavPropertyValue::new(
WebDavProperty::GetLastModified,
DavValue::Rfc1123Date(Rfc1123DateTime::new(
DateTime::parse_rfc822("Mon, 12 Jan 1998 09:25:56 GMT")
.unwrap()
.to_timestamp(),
)),
),
DavPropertyValue::new(WebDavProperty::ResourceType, DavValue::Null),
DavPropertyValue::new(
WebDavProperty::SupportedLock,
SupportedLock::default(),
),
])],
),
])
.to_string(),
// 004.xml
MultiStatus::new(vec![
Response::new_status(
["http://www.example.com/container/resource3"],
StatusCode::LOCKED,
)
.with_error(BaseCondition::LockTokenSubmitted(List(vec![]))),
])
.to_string(),
// 005.xml
PropResponse::new(vec![DavPropertyValue::new(
WebDavProperty::LockDiscovery,
vec![
ActiveLock::new(
"http://example.com/workspace/webdav/proposal.doc",
LockScope::Exclusive,
)
.with_owner(DeadProperty(vec![
DeadPropertyTag::ElementStart(DeadElementTag {
name: "D:href".to_string(),
attrs: None,
}),
DeadPropertyTag::Text("http://example.org/~ejw/contact.html".to_string()),
DeadPropertyTag::ElementEnd,
]))
.with_timeout(604800)
.with_lock_token("urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4"),
],
)])
.to_string(),
// 006.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/container/",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::LockDiscovery,
vec![
ActiveLock::new("http://www.example.com/container/", LockScope::Shared)
.with_owner(DeadProperty(vec![DeadPropertyTag::Text(
"Jane Smith".to_string(),
)]))
.with_depth(Depth::Zero)
.with_lock_token("urn:uuid:f81de2ad-7f3d-a1b2-4f3c-00a0c91a9d76"),
],
)])],
)])
.to_string(),
// 007.xml
ErrorResponse::new(BaseCondition::LockTokenSubmitted(List(vec![Href(
"/workspace/webdav/".to_string(),
)])))
.to_string(),
// 008.xml
MultiStatus::new(vec![
Response::new_propstat(
"http://cal.example.com/bernard/work/abcd2.ics",
vec![PropStat::new_list(vec![
DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd2\""),
DavPropertyValue::new(
CalDavProperty::CalendarData(Default::default()),
DavValue::CData(
r#"BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060106T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060106T120000
SUMMARY:Event #2 bis bis
UID:[email protected]
END:VEVENT
END:VCALENDAR
"#
.to_string(),
),
),
])],
),
Response::new_propstat(
"http://cal.example.com/bernard/work/abcd3.ics",
vec![PropStat::new_list(vec![
DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd3\""),
DavPropertyValue::new(
CalDavProperty::CalendarData(Default::default()),
DavValue::CData(
r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
SUMMARY:Event #3
UID:[email protected]
END:VEVENT
END:VCALENDAR
"#
.to_string(),
),
),
])],
),
])
.with_namespace(Namespace::CalDav)
.to_string(),
// 009.xml
MkColResponse::new(vec![PropStat::new_list(vec![
DavPropertyValue::new(WebDavProperty::ResourceType, DavValue::Null),
DavPropertyValue::new(WebDavProperty::DisplayName, DavValue::Null),
DavPropertyValue::new(CardDavProperty::AddressbookDescription, DavValue::Null),
])])
.with_namespace(Namespace::CardDav)
.to_string(),
// 010.xml
MultiStatus::new(vec![Response::new_propstat(
"/home/bernard/addressbook/v102.vcf",
vec![PropStat::new_list(vec![
DavPropertyValue::new(WebDavProperty::GetETag, "\"23ba4d-ff11fb\""),
DavPropertyValue::new(
CardDavProperty::AddressData {
properties: Default::default(),
version: None,
},
DavValue::CData(
r#"BEGIN:VCARD
VERSION:3.0
NICKNAME:me
UID:[email protected]
FN:Cyrus Daboo
EMAIL:[email protected]
END:VCARD
"#
.to_string(),
),
),
])],
)])
.with_namespace(Namespace::CardDav)
.to_string(),
// 011.xml
MultiStatus::new(vec![
Response::new_status(
["/home/bernard/addressbook/"],
StatusCode::INSUFFICIENT_STORAGE,
)
.with_error(BaseCondition::NumberOfMatchesWithinLimit)
.with_response_description("Only two matching records were returned"),
Response::new_propstat(
"/home/bernard/addressbook/v102.vcf",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::GetETag,
"\"23ba4d-ff11fb\"",
)])],
),
Response::new_propstat(
"/home/bernard/addressbook/v104.vcf",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::GetETag,
"\"23ba4d-ff11fc\"",
)])],
),
])
.with_namespace(Namespace::CardDav)
.to_string(),
// 012.xml
ErrorResponse::new(BaseCondition::NeedPrivileges(List(vec![
Resource::new("/a", Privilege::Unbind),
Resource::new("/c", Privilege::Bind),
])))
.to_string(),
// 013.xml
PrincipalSearchPropertySet::new(vec![
PrincipalSearchProperty::new(WebDavProperty::DisplayName, "Full name"),
PrincipalSearchProperty::new(WebDavProperty::DisplayName, "Job title"),
])
.to_string(),
// 014.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/papers/",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::SupportedPrivilegeSet,
vec![
SupportedPrivilege::new(Privilege::All, "Any operation")
.with_abstract()
.with_supported_privilege(
SupportedPrivilege::new(Privilege::Read, "Read any object")
.with_supported_privilege(
SupportedPrivilege::new(Privilege::ReadAcl, "Read ACL")
.with_abstract(),
)
.with_supported_privilege(
SupportedPrivilege::new(
Privilege::ReadCurrentUserPrivilegeSet,
"Read current user privilege set property",
)
.with_abstract(),
),
)
.with_supported_privilege(
SupportedPrivilege::new(Privilege::Write, "Write any object")
.with_supported_privilege(
SupportedPrivilege::new(Privilege::WriteAcl, "Write ACL")
.with_abstract(),
)
.with_supported_privilege(SupportedPrivilege::new(
Privilege::WriteProperties,
"Write properties",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::WriteContent,
"Write resource content",
)),
)
.with_supported_privilege(SupportedPrivilege::new(
Privilege::Unlock,
"Unlock resource",
)),
],
)])],
)])
.to_string(),
// 015.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/papers/",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::CurrentUserPrivilegeSet,
vec![Privilege::Read],
)])],
)])
.to_string(),
// 016.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/papers/",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::Acl,
vec![
Ace::new(
Principal::Href(Href(
"http://www.example.com/acl/groups/maintainers".to_string(),
)),
GrantDeny::grant(vec![Privilege::Write]),
),
Ace::new(Principal::All, GrantDeny::grant(vec![Privilege::Read])),
],
)])],
)])
.to_string(),
// 017.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/papers/",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::AclRestrictions,
AclRestrictions::new()
.with_grant_only()
.with_required_principal(RequiredPrincipal::All),
)])],
)])
.to_string(),
// 018.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/papers/",
vec![PropStat::new_list(vec![DavPropertyValue::new(
WebDavProperty::PrincipalCollectionSet,
vec![
Href("http://www.example.com/acl/users/".to_string()),
Href("http://www.example.com/acl/groups/".to_string()),
],
)])],
)])
.to_string(),
// 019.xml
MultiStatus::new(vec![Response::new_propstat(
"http://www.example.com/top/container/",
vec![PropStat::new_list(vec![
DavPropertyValue::new(
WebDavProperty::Owner,
vec![Href("http://www.example.com/users/gclemm".to_string())],
),
DavPropertyValue::new(
WebDavProperty::SupportedPrivilegeSet,
vec![
SupportedPrivilege::new(Privilege::All, "Any operation")
.with_abstract()
.with_supported_privilege(SupportedPrivilege::new(
Privilege::Read,
"Read any object",
))
.with_supported_privilege(
SupportedPrivilege::new(Privilege::Write, "Write any object")
.with_abstract(),
)
.with_supported_privilege(SupportedPrivilege::new(
Privilege::ReadAcl,
"Read the ACL",
))
.with_supported_privilege(SupportedPrivilege::new(
Privilege::WriteAcl,
"Write the ACL",
)),
],
),
DavPropertyValue::new(
WebDavProperty::CurrentUserPrivilegeSet,
vec![Privilege::Read, Privilege::ReadAcl],
),
DavPropertyValue::new(
WebDavProperty::Acl,
vec![
Ace::new(
Principal::Href(Href(
"http://www.example.com/users/esedlar".to_string(),
)),
GrantDeny::grant(vec![
Privilege::Read,
Privilege::Write,
Privilege::ReadAcl,
]),
),
Ace::new(
Principal::Href(Href(
"http://www.example.com/groups/mrktng".to_string(),
)),
GrantDeny::deny(vec![Privilege::Read]),
),
Ace::new(
Principal::Property(List(vec![DavPropertyValue::new(
WebDavProperty::Owner,
DavValue::Null,
)])),
GrantDeny::grant(vec![Privilege::ReadAcl, Privilege::WriteAcl]),
),
Ace::new(Principal::All, GrantDeny::grant(vec![Privilege::Read]))
.with_inherited("http://www.example.com/top"),
],
),
])],
)])
.to_string(),
// 020.xml
ScheduleResponse {
items: List(vec![
ScheduleResponseItem {
recipient: Href("mailto:[email protected]".to_string()),
request_status: "2.0;Success".into(),
calendar_data: Some("BEGIN:VCALENDAR".to_string()),
},
ScheduleResponseItem {
recipient: Href("mailto:[email protected]".to_string()),
request_status: "2.0;Success".into(),
calendar_data: Some("END:VCALENDAR".to_string()),
},
ScheduleResponseItem {
recipient: Href("mailto:[email protected]".to_string()),
request_status: "3.7;Invalid calendar user".into(),
calendar_data: None,
},
]),
}
.to_string(),
]
.into_iter()
.enumerate()
{
let xml =
std::fs::read_to_string(format!("resources/responses/{:03}.xml", num + 1)).unwrap();
let mut output_token = Tokenizer::new(test.as_bytes());
let mut expected_token = Tokenizer::new(xml.as_bytes());
let mut output_tokens = Vec::new();
let mut expected_tokens = Vec::new();
for (tokens, tokenizer) in [
(&mut output_tokens, &mut output_token),
(&mut expected_tokens, &mut expected_token),
] {
while let Ok(token) = tokenizer.token() {
if token == Token::Eof {
break;
}
match (tokens.last_mut(), token) {
(Some(Token::Text(text)), Token::Text(new_text)) => {
*text = format!("{}{}", text, new_text).into();
}
(_, element) => {
tokens.push(element.into_owned());
}
}
}
}
assert!(!output_tokens.is_empty());
assert!(!expected_tokens.is_empty());
assert_eq!(output_tokens.len(), expected_tokens.len());
for (output, expected) in output_tokens.iter().zip(expected_tokens.iter()) {
if output != expected {
eprintln!("{test}");
}
assert_eq!(output, expected, "failed for {:03}.xml", num + 1);
}
}
}
#[test]
fn escape_cdata() {
for (test, expected) in [
("", "<![CDATA[]]>"),
("hello", "<![CDATA[hello]]>"),
("hello world", "<![CDATA[hello world]]>"),
("<hello>", "<![CDATA[<hello>]]>"),
("&hello;", "<![CDATA[&hello;]]>"),
("'hello'", "<![CDATA['hello']]>"),
("\"hello\"", "<![CDATA[\"hello\"]]>"),
("<>&'\"", "<![CDATA[<>&'\"]]>"),
(">", "<![CDATA[>]]>"),
("]]>]", "<![CDATA[]]]]><![CDATA[>]]]>"),
("]]>", "<![CDATA[]]]]><![CDATA[>]]>"),
("hello]]>world", "<![CDATA[hello]]]]><![CDATA[>world]]>"),
(
"hello]]><nasty-xml>pure-evil</nasty-xml>",
"<![CDATA[hello]]]]><![CDATA[><nasty-xml>pure-evil</nasty-xml>]]>",
),
] {
let mut output = String::new();
test.write_cdata_escaped_to(&mut output).unwrap();
assert_eq!(output, expected, "failed for input: {test:?}");
}
}
}
@@ -0,0 +1,161 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::schema::{
Namespace, Namespaces,
response::{
Condition, Href, List, Location, MultiStatus, PropStat, Response, ResponseDescription,
ResponseType, Status, SyncToken,
},
};
use hyper::StatusCode;
use std::fmt::Display;
impl Display for MultiStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><D:multistatus {}>{}",
self.namespaces, self.response
)?;
if let Some(response_description) = &self.response_description {
write!(f, "{response_description}")?;
}
if let Some(sync_token) = &self.sync_token {
write!(f, "{sync_token}")?;
}
write!(f, "</D:multistatus>")
}
}
impl Display for Response {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:response>")?;
self.href.fmt(f)?;
self.typ.fmt(f)?;
if let Some(error) = &self.error {
error.fmt(f)?;
}
if let Some(response_description) = &self.response_description {
response_description.fmt(f)?;
}
if let Some(location) = &self.location {
location.fmt(f)?;
}
write!(f, "</D:response>")
}
}
impl Display for ResponseType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResponseType::PropStat(list) => list.fmt(f),
ResponseType::Status { href, status } => {
href.fmt(f)?;
status.fmt(f)
}
}
}
}
impl MultiStatus {
pub fn new(response: Vec<Response>) -> Self {
MultiStatus {
namespaces: Namespaces::default(),
response: List(response),
response_description: None,
sync_token: None,
}
}
pub fn with_response(mut self, response: Response) -> Self {
self.response.0.push(response);
self
}
pub fn not_found(href: impl Into<String>) -> Self {
let mut response = Self::new(Vec::with_capacity(1));
response.response.0.push(
Response::new_status([href], StatusCode::NOT_FOUND)
.with_response_description("No resources found"),
);
response
}
pub fn add_response(&mut self, response: Response) {
self.response.0.push(response);
}
pub fn with_response_description(mut self, response_description: impl Into<String>) -> Self {
self.response_description = Some(ResponseDescription(response_description.into()));
self
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespaces.set(namespace);
self
}
pub fn set_namespace(&mut self, namespace: Namespace) {
self.namespaces.set(namespace);
}
pub fn with_sync_token(mut self, sync_token: impl Into<String>) -> Self {
self.sync_token = Some(SyncToken(sync_token.into()));
self
}
pub fn set_sync_token(&mut self, sync_token: impl Into<String>) {
self.sync_token = Some(SyncToken(sync_token.into()));
}
}
impl Response {
pub fn new_propstat(href: impl Into<Href>, propstat: Vec<PropStat>) -> Self {
Response {
href: href.into(),
typ: ResponseType::PropStat(List(propstat)),
error: None,
response_description: None,
location: None,
}
}
pub fn new_status<T, H>(href: T, status: StatusCode) -> Self
where
T: IntoIterator<Item = H>,
H: Into<String>,
{
let mut href = href.into_iter().map(|h| Href(h.into()));
Response {
href: href.next().unwrap(),
typ: ResponseType::Status {
href: List(href.collect()),
status: Status(status),
},
error: None,
response_description: None,
location: None,
}
}
pub fn with_error(mut self, error: impl Into<Condition>) -> Self {
self.error = Some(error.into());
self
}
pub fn with_response_description(mut self, response_description: impl Into<String>) -> Self {
self.response_description = Some(ResponseDescription(response_description.into()));
self
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(Location(Href(location.into())));
self
}
}
+440
View File
@@ -0,0 +1,440 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{XmlCdataEscape, XmlEscape};
use crate::{
responses::DeadPropertyFormat,
schema::{
Namespace, Namespaces,
property::{
ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue,
LockDiscovery, LockEntry, PrincipalProperty, Privilege, ReportSet, ResourceType,
Rfc1123DateTime, SupportedCollation, SupportedLock, WebDavProperty,
},
request::DavPropertyValue,
response::{Ace, AclRestrictions, Href, List, PropResponse, SupportedPrivilege},
},
};
use calcard::icalendar::ICalendarComponentType;
use mail_parser::{
DateTime,
parsers::fields::date::{DOW, MONTH},
};
use std::fmt::Display;
use types::dead_property::DeadProperty;
impl Display for PropResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><D:prop {}>{}</D:prop>",
self.namespaces, self.properties
)
}
}
impl Display for DavPropertyValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (name, attrs) = self.property.tag_name();
write!(f, "<{}", name)?;
if let Some(attrs) = attrs {
write!(f, " {attrs}")?;
}
if !matches!(self.value, DavValue::Null) {
write!(f, ">{}</{}>", self.value, name)
} else {
write!(f, "/>")
}
}
}
impl Display for Rfc1123DateTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let dt = DateTime::from_timestamp(self.0);
write!(
f,
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
DOW[dt.day_of_week() as usize],
dt.day,
MONTH
.get(dt.month.saturating_sub(1) as usize)
.copied()
.unwrap_or_default(),
dt.year,
dt.hour,
dt.minute,
dt.second,
)
}
}
impl Display for DavValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DavValue::Timestamp(v) => {
let dt = DateTime::from_timestamp(*v);
write!(
f,
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
)
}
DavValue::Rfc1123Date(v) => v.fmt(f),
DavValue::Uint64(v) => v.fmt(f),
DavValue::String(v) => v.write_escaped_to(f),
DavValue::ResourceTypes(v) => v.fmt(f),
DavValue::ActiveLocks(v) => v.fmt(f),
DavValue::LockEntries(v) => v.fmt(f),
DavValue::ReportSets(v) => v.fmt(f),
DavValue::CData(v) => v.write_cdata_escaped_to(f),
DavValue::Components(v) => v.fmt(f),
DavValue::Collations(v) => v.fmt(f),
DavValue::Href(v) => v.fmt(f),
DavValue::PrivilegeSet(v) => v.fmt(f),
DavValue::Privileges(v) => v.fmt(f),
DavValue::Acl(v) => v.fmt(f),
DavValue::AclRestrictions(v) => v.fmt(f),
DavValue::DeadProperty(v) => v.fmt(f),
DavValue::SupportedAddressData => {
write!(
f,
concat!(
"<B:address-data-type content-type=\"text/vcard\" version=\"4.0\"/>",
"<B:address-data-type content-type=\"text/vcard\" version=\"3.0\"/>",
"<B:address-data-type content-type=\"text/vcard\" version=\"2.1\"/>",
)
)
}
DavValue::SupportedCalendarData => {
write!(
f,
concat!(
"<A:calendar-data-type content-type=\"text/calendar\" version=\"2.0\"/>",
"<A:calendar-data-type content-type=\"text/calendar\" version=\"1.0\"/>",
)
)
}
DavValue::Response(v) => v.fmt(f),
DavValue::VCard(_) | DavValue::ICalendar(_) | DavValue::Null => Ok(()),
}
}
}
impl DavValue {
pub fn all_calendar_components() -> Self {
DavValue::Components(List(vec![
Comp(ICalendarComponentType::VEvent),
Comp(ICalendarComponentType::VTodo),
Comp(ICalendarComponentType::VJournal),
Comp(ICalendarComponentType::VFreebusy),
Comp(ICalendarComponentType::VTimezone),
Comp(ICalendarComponentType::VAlarm),
Comp(ICalendarComponentType::Standard),
Comp(ICalendarComponentType::Daylight),
Comp(ICalendarComponentType::VAvailability),
Comp(ICalendarComponentType::Available),
Comp(ICalendarComponentType::Participant),
Comp(ICalendarComponentType::VLocation),
Comp(ICalendarComponentType::VResource),
]))
}
}
impl DavProperty {
fn tag_name(&self) -> (&str, Option<&str>) {
(
match self {
DavProperty::WebDav(prop) => match prop {
WebDavProperty::CreationDate => "D:creationdate",
WebDavProperty::DisplayName => "D:displayname",
WebDavProperty::GetContentLanguage => "D:getcontentlanguage",
WebDavProperty::GetContentLength => "D:getcontentlength",
WebDavProperty::GetContentType => "D:getcontenttype",
WebDavProperty::GetETag => "D:getetag",
WebDavProperty::GetLastModified => "D:getlastmodified",
WebDavProperty::ResourceType => "D:resourcetype",
WebDavProperty::LockDiscovery => "D:lockdiscovery",
WebDavProperty::SupportedLock => "D:supportedlock",
WebDavProperty::CurrentUserPrincipal => "D:current-user-principal",
WebDavProperty::QuotaAvailableBytes => "D:quota-available-bytes",
WebDavProperty::QuotaUsedBytes => "D:quota-used-bytes",
WebDavProperty::SupportedReportSet => "D:supported-report-set",
WebDavProperty::SyncToken => "D:sync-token",
WebDavProperty::Owner => "D:owner",
WebDavProperty::Group => "D:group",
WebDavProperty::SupportedPrivilegeSet => "D:supported-privilege-set",
WebDavProperty::CurrentUserPrivilegeSet => "D:current-user-privilege-set",
WebDavProperty::Acl => "D:acl",
WebDavProperty::AclRestrictions => "D:acl-restrictions",
WebDavProperty::InheritedAclSet => "D:inherited-acl-set",
WebDavProperty::PrincipalCollectionSet => "D:principal-collection-set",
WebDavProperty::GetCTag => "C:getctag",
},
DavProperty::CardDav(prop) => match prop {
CardDavProperty::AddressbookDescription => "B:addressbook-description",
CardDavProperty::SupportedAddressData => "B:supported-address-data",
CardDavProperty::SupportedCollationSet => "B:supported-collation-set",
CardDavProperty::MaxResourceSize => "B:max-resource-size",
CardDavProperty::AddressData { .. } => "B:address-data",
},
DavProperty::CalDav(prop) => match prop {
CalDavProperty::CalendarDescription => "A:calendar-description",
CalDavProperty::CalendarTimezone => "A:calendar-timezone",
CalDavProperty::SupportedCalendarComponentSet => {
"A:supported-calendar-component-set"
}
CalDavProperty::SupportedCalendarData => "A:supported-calendar-data",
CalDavProperty::SupportedCollationSet => "A:supported-collation-set",
CalDavProperty::MaxResourceSize => "A:max-resource-size",
CalDavProperty::MinDateTime => "A:min-date-time",
CalDavProperty::MaxDateTime => "A:max-date-time",
CalDavProperty::MaxInstances => "A:max-instances",
CalDavProperty::MaxAttendeesPerInstance => "A:max-attendees-per-instance",
CalDavProperty::CalendarData(_) => "A:calendar-data",
CalDavProperty::TimezoneServiceSet => "A:timezone-service-set",
CalDavProperty::TimezoneId => "A:calendar-timezone-id",
CalDavProperty::ScheduleDefaultCalendarURL => "A:schedule-default-calendar-URL",
CalDavProperty::ScheduleTag => "A:schedule-tag",
CalDavProperty::ScheduleCalendarTransp => "A:schedule-calendar-transp",
},
DavProperty::Principal(prop) => match prop {
PrincipalProperty::AlternateURISet => "D:alternate-URI-set",
PrincipalProperty::PrincipalURL => "D:principal-URL",
PrincipalProperty::GroupMemberSet => "D:group-member-set",
PrincipalProperty::GroupMembership => "D:group-membership",
PrincipalProperty::CalendarHomeSet => "A:calendar-home-set",
PrincipalProperty::AddressbookHomeSet => "B:addressbook-home-set",
PrincipalProperty::PrincipalAddress => "B:principal-address",
PrincipalProperty::CalendarUserAddressSet => "A:calendar-user-address-set",
PrincipalProperty::CalendarUserType => "A:calendar-user-type",
PrincipalProperty::ScheduleInboxURL => "A:schedule-inbox-URL",
PrincipalProperty::ScheduleOutboxURL => "A:schedule-outbox-URL",
},
DavProperty::DeadProperty(dead) => {
return (dead.name.as_str(), dead.attrs.as_deref());
}
},
None,
)
}
pub fn namespace(&self) -> Namespace {
match self {
DavProperty::WebDav(WebDavProperty::GetCTag) => Namespace::CalendarServer,
DavProperty::CardDav(_)
| DavProperty::Principal(
PrincipalProperty::AddressbookHomeSet | PrincipalProperty::PrincipalAddress,
) => Namespace::CardDav,
DavProperty::CalDav(_)
| DavProperty::Principal(
PrincipalProperty::CalendarHomeSet
| PrincipalProperty::CalendarUserAddressSet
| PrincipalProperty::CalendarUserType
| PrincipalProperty::ScheduleInboxURL
| PrincipalProperty::ScheduleOutboxURL,
) => Namespace::CalDav,
_ => Namespace::Dav,
}
}
}
impl AsRef<str> for DavProperty {
fn as_ref(&self) -> &str {
self.tag_name().0
}
}
impl Display for ReportSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("<D:supported-report><D:report>")?;
match self {
ReportSet::SyncCollection => write!(f, "<D:sync-collection/>"),
ReportSet::ExpandProperty => write!(f, "<D:expand-property/>"),
ReportSet::AddressbookQuery => write!(f, "<B:addressbook-query/>"),
ReportSet::AddressbookMultiGet => write!(f, "<B:addressbook-multiget/>"),
ReportSet::CalendarQuery => write!(f, "<A:calendar-query/>"),
ReportSet::CalendarMultiGet => write!(f, "<A:calendar-multiget/>"),
ReportSet::FreeBusyQuery => write!(f, "<A:free-busy-query/>"),
ReportSet::AclPrincipalPropSet => write!(f, "<D:acl-principal-prop-set/>"),
ReportSet::PrincipalMatch => write!(f, "<D:principal-match/>"),
ReportSet::PrincipalPropertySearch => write!(f, "<D:principal-property-search/>"),
ReportSet::PrincipalSearchPropertySet => {
write!(f, "<D:principal-search-property-set/>")
}
}?;
f.write_str("</D:report></D:supported-report>")
}
}
impl Display for DavProperty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (name, attrs) = self.tag_name();
if let Some(attrs) = attrs {
write!(f, "<{name} {attrs}/>")
} else {
write!(f, "<{name}/>")
}
}
}
impl PropResponse {
pub fn new(properties: Vec<DavPropertyValue>) -> Self {
PropResponse {
namespaces: Namespaces::default(),
properties: List(properties),
}
}
pub fn with_namespace(mut self, namespace: Namespace) -> Self {
self.namespaces.set(namespace);
self
}
}
impl From<WebDavProperty> for DavProperty {
fn from(prop: WebDavProperty) -> Self {
DavProperty::WebDav(prop)
}
}
impl From<CardDavProperty> for DavProperty {
fn from(prop: CardDavProperty) -> Self {
DavProperty::CardDav(prop)
}
}
impl From<CalDavProperty> for DavProperty {
fn from(prop: CalDavProperty) -> Self {
DavProperty::CalDav(prop)
}
}
impl From<String> for DavValue {
fn from(v: String) -> Self {
DavValue::String(v)
}
}
impl From<&str> for DavValue {
fn from(v: &str) -> Self {
DavValue::String(v.to_string())
}
}
impl From<u64> for DavValue {
fn from(v: u64) -> Self {
DavValue::Uint64(v)
}
}
impl From<DateTime> for DavValue {
fn from(v: DateTime) -> Self {
DavValue::Timestamp(v.to_timestamp())
}
}
impl From<Vec<ResourceType>> for DavValue {
fn from(v: Vec<ResourceType>) -> Self {
DavValue::ResourceTypes(List(v))
}
}
impl From<Vec<ReportSet>> for DavValue {
fn from(v: Vec<ReportSet>) -> Self {
DavValue::ReportSets(List(v))
}
}
impl From<Vec<Comp>> for DavValue {
fn from(v: Vec<Comp>) -> Self {
DavValue::Components(List(v))
}
}
impl From<Vec<SupportedCollation>> for DavValue {
fn from(v: Vec<SupportedCollation>) -> Self {
DavValue::Collations(List(v))
}
}
impl From<SupportedLock> for DavValue {
fn from(v: SupportedLock) -> Self {
DavValue::LockEntries(v.0)
}
}
impl From<Vec<LockEntry>> for DavValue {
fn from(v: Vec<LockEntry>) -> Self {
DavValue::LockEntries(List(v))
}
}
impl From<Vec<ActiveLock>> for DavValue {
fn from(v: Vec<ActiveLock>) -> Self {
DavValue::ActiveLocks(List(v))
}
}
impl From<LockDiscovery> for DavValue {
fn from(v: LockDiscovery) -> Self {
DavValue::ActiveLocks(v.0)
}
}
impl From<Vec<SupportedPrivilege>> for DavValue {
fn from(v: Vec<SupportedPrivilege>) -> Self {
DavValue::PrivilegeSet(List(v))
}
}
impl From<Vec<Privilege>> for DavValue {
fn from(v: Vec<Privilege>) -> Self {
DavValue::Privileges(List(v))
}
}
impl From<Vec<Href>> for DavValue {
fn from(v: Vec<Href>) -> Self {
DavValue::Href(List(v))
}
}
impl From<Vec<Ace>> for DavValue {
fn from(v: Vec<Ace>) -> Self {
DavValue::Acl(List(v))
}
}
impl From<AclRestrictions> for DavValue {
fn from(v: AclRestrictions) -> Self {
DavValue::AclRestrictions(v)
}
}
impl From<DeadProperty> for DavValue {
fn from(v: DeadProperty) -> Self {
DavValue::DeadProperty(v)
}
}
impl DavPropertyValue {
pub fn new(property: impl Into<DavProperty>, value: impl Into<DavValue>) -> Self {
DavPropertyValue {
property: property.into(),
value: value.into(),
}
}
pub fn empty(property: impl Into<DavProperty>) -> Self {
DavPropertyValue {
property: property.into(),
value: DavValue::Null,
}
}
}
@@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::schema::{
request::DavPropertyValue,
response::{Condition, List, Prop, PropStat, ResponseDescription, Status},
};
use hyper::StatusCode;
use std::fmt::Display;
impl Display for PropStat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:propstat>")?;
self.prop.fmt(f)?;
self.status.fmt(f)?;
if let Some(error) = &self.error {
error.fmt(f)?;
}
if let Some(response_description) = &self.response_description {
response_description.fmt(f)?;
}
write!(f, "</D:propstat>")
}
}
impl Display for Prop {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<D:prop>{}</D:prop>", self.0)
}
}
impl PropStat {
#[cfg(test)]
pub(crate) fn new(prop: impl Into<DavPropertyValue>) -> Self {
PropStat {
prop: Prop(List(vec![prop.into()])),
status: Status(StatusCode::OK),
error: None,
response_description: None,
}
}
pub fn new_list(props: Vec<DavPropertyValue>) -> Self {
PropStat {
prop: Prop(List(props)),
status: Status(StatusCode::OK),
error: None,
response_description: None,
}
}
pub fn with_prop(mut self, prop: impl Into<DavPropertyValue>) -> Self {
self.prop.0.0.push(prop.into());
self
}
pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = Status(status);
self
}
pub fn with_error(mut self, error: impl Into<Condition>) -> Self {
self.error = Some(error.into());
self
}
pub fn with_response_description(mut self, response_description: impl Into<String>) -> Self {
self.response_description = Some(ResponseDescription(response_description.into()));
self
}
}
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
responses::{XmlCdataEscape, XmlEscape},
schema::{
Namespaces,
response::{ScheduleResponse, ScheduleResponseItem},
},
};
use std::fmt::Display;
const NAMESPACE: Namespaces = Namespaces {
cal: true,
card: false,
cs: false,
};
impl Display for ScheduleResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
write!(
f,
"<A:schedule-response {NAMESPACE}>{}</A:schedule-response>",
self.items
)
}
}
impl Display for ScheduleResponseItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<A:response>")?;
write!(f, "<A:recipient>{}</A:recipient>", self.recipient)?;
write!(f, "<A:request-status>")?;
self.request_status.write_escaped_to(f)?;
write!(f, "</A:request-status>")?;
if let Some(calendar_data) = &self.calendar_data {
write!(f, "<A:calendar-data>")?;
calendar_data.write_cdata_escaped_to(f)?;
write!(f, "</A:calendar-data>")?;
}
write!(f, "</A:response>")
}
}
File diff suppressed because it is too large Load Diff
+441
View File
@@ -0,0 +1,441 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
Collation, Namespace,
request::DavPropertyValue,
response::{Ace, AclRestrictions, Href, List, Response, SupportedPrivilege},
};
use crate::{Depth, Timeout};
use calcard::{
icalendar::{ICalendar, ICalendarComponentType, ICalendarProperty},
vcard::{VCard, VCardProperty, VCardVersion},
};
use types::{
TimeRange,
dead_property::{DeadElementTag, DeadProperty},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type", content = "data"))]
pub enum DavProperty {
WebDav(WebDavProperty),
CardDav(CardDavProperty),
CalDav(CalDavProperty),
Principal(PrincipalProperty),
DeadProperty(DeadElementTag),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type", content = "data"))]
pub enum WebDavProperty {
CreationDate,
DisplayName,
GetContentLanguage,
GetContentLength,
GetContentType,
GetETag,
GetLastModified,
ResourceType,
LockDiscovery,
SupportedLock,
SupportedReportSet,
CurrentUserPrincipal,
// Quota properties
QuotaAvailableBytes,
QuotaUsedBytes,
// Sync properties
SyncToken,
// ACL properties (all protected)
Owner,
Group,
SupportedPrivilegeSet,
CurrentUserPrivilegeSet,
Acl,
AclRestrictions,
InheritedAclSet,
PrincipalCollectionSet,
// Apple proprietary properties
GetCTag,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type", content = "data"))]
pub enum CardDavProperty {
AddressbookDescription,
SupportedAddressData,
SupportedCollationSet,
MaxResourceSize,
AddressData {
properties: Vec<CardDavPropertyName>,
#[cfg_attr(test, serde(skip))]
version: Option<VCardVersion>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct CardDavPropertyName {
pub group: Option<String>,
pub name: VCardProperty,
pub no_value: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type", content = "data"))]
pub enum CalDavProperty {
CalendarDescription,
CalendarTimezone,
SupportedCalendarComponentSet,
SupportedCalendarData,
SupportedCollationSet,
MaxResourceSize,
MinDateTime,
MaxDateTime,
MaxInstances,
MaxAttendeesPerInstance,
CalendarData(CalendarData),
TimezoneServiceSet,
TimezoneId,
ScheduleDefaultCalendarURL,
ScheduleTag,
ScheduleCalendarTransp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type", content = "data"))]
pub enum PrincipalProperty {
AlternateURISet,
PrincipalURL,
GroupMemberSet,
GroupMembership,
CalendarHomeSet,
AddressbookHomeSet,
PrincipalAddress,
CalendarUserAddressSet,
CalendarUserType,
ScheduleInboxURL,
ScheduleOutboxURL,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct CalendarData {
pub properties: Vec<CalDavPropertyName>,
pub expand: Option<TimeRange>,
pub limit_recurrence: Option<TimeRange>,
pub limit_freebusy: Option<TimeRange>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct CalDavPropertyName {
pub component: Option<ICalendarComponentType>,
pub name: Option<ICalendarProperty>,
pub no_value: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct Rfc1123DateTime(pub(crate) i64);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum DavValue {
Timestamp(i64),
Rfc1123Date(Rfc1123DateTime),
Uint64(u64),
String(String),
CData(String),
ResourceTypes(List<ResourceType>),
ActiveLocks(List<ActiveLock>),
LockEntries(List<LockEntry>),
ReportSets(List<ReportSet>),
ICalendar(ICalendar),
VCard(VCard),
Components(List<Comp>),
Collations(List<SupportedCollation>),
PrivilegeSet(List<SupportedPrivilege>),
Privileges(List<Privilege>),
Href(List<Href>),
Acl(List<Ace>),
AclRestrictions(AclRestrictions),
Response(Box<Response>),
DeadProperty(DeadProperty),
SupportedAddressData,
SupportedCalendarData,
Null,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum ReportSet {
SyncCollection,
ExpandProperty,
AddressbookQuery,
AddressbookMultiGet,
CalendarQuery,
CalendarMultiGet,
FreeBusyQuery,
AclPrincipalPropSet,
PrincipalMatch,
PrincipalPropertySearch,
PrincipalSearchPropertySet,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct Comp(pub ICalendarComponentType);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct SupportedCollation {
pub collation: Collation,
pub namespace: Namespace,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum ResourceType {
Collection,
Principal,
AddressBook,
Calendar,
ScheduleInbox,
ScheduleOutbox,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct LockDiscovery(pub List<ActiveLock>);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct ActiveLock {
pub lock_scope: LockScope,
pub lock_type: LockType,
pub depth: Depth,
pub owner: Option<DeadProperty>,
pub timeout: Timeout,
pub lock_token: Option<Href>,
pub lock_root: Href,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct SupportedLock(pub List<LockEntry>);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct LockEntry {
pub lock_scope: LockScope,
pub lock_type: LockType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum LockType {
Write,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum LockScope {
Exclusive,
Shared,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum Privilege {
Read,
Write,
WriteProperties,
WriteContent,
Unlock,
ReadAcl,
ReadCurrentUserPrivilegeSet,
WriteAcl,
Bind,
Unbind,
All,
ReadFreeBusy,
ScheduleDeliver,
ScheduleDeliverInvite,
ScheduleDeliverReply,
ScheduleQueryFreeBusy,
ScheduleSend,
ScheduleSendInvite,
ScheduleSendReply,
ScheduleSendFreeBusy,
}
impl Privilege {
pub fn all(is_calendar: bool) -> Vec<Privilege> {
if is_calendar {
vec![
Privilege::All,
Privilege::Read,
Privilege::Write,
Privilege::WriteProperties,
Privilege::WriteContent,
Privilege::Unlock,
Privilege::ReadAcl,
Privilege::ReadCurrentUserPrivilegeSet,
Privilege::WriteAcl,
Privilege::Bind,
Privilege::Unbind,
Privilege::ReadFreeBusy,
]
} else {
vec![
Privilege::All,
Privilege::Read,
Privilege::Write,
Privilege::WriteProperties,
Privilege::WriteContent,
Privilege::Unlock,
Privilege::ReadAcl,
Privilege::ReadCurrentUserPrivilegeSet,
Privilege::WriteAcl,
Privilege::Bind,
Privilege::Unbind,
]
}
}
pub fn scheduling(is_inbox: bool, is_owner: bool) -> Vec<Privilege> {
let mut privileges = if is_inbox {
vec![
Privilege::Read,
Privilege::ReadCurrentUserPrivilegeSet,
Privilege::ScheduleDeliver,
Privilege::ScheduleDeliverInvite,
Privilege::ScheduleDeliverReply,
Privilege::ScheduleQueryFreeBusy,
]
} else {
vec![
Privilege::Read,
Privilege::ReadCurrentUserPrivilegeSet,
Privilege::ScheduleSend,
Privilege::ScheduleSendInvite,
Privilege::ScheduleSendReply,
Privilege::ScheduleSendFreeBusy,
]
};
if is_owner {
privileges.extend([
Privilege::All,
Privilege::Write,
Privilege::WriteProperties,
Privilege::WriteContent,
Privilege::ReadAcl,
Privilege::WriteAcl,
]);
}
privileges
}
}
impl From<DavProperty> for DavPropertyValue {
fn from(value: DavProperty) -> Self {
DavPropertyValue {
property: value,
value: DavValue::Null,
}
}
}
impl Rfc1123DateTime {
pub fn new(timestamp: i64) -> Self {
Self(timestamp)
}
}
impl DavProperty {
pub const ALL_PROPS: [DavProperty; 11] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
DavProperty::WebDav(WebDavProperty::GetContentLength),
DavProperty::WebDav(WebDavProperty::GetContentType),
];
pub fn is_all_prop(&self) -> bool {
matches!(
self,
DavProperty::WebDav(WebDavProperty::CreationDate)
| DavProperty::WebDav(WebDavProperty::DisplayName)
| DavProperty::WebDav(WebDavProperty::GetETag)
| DavProperty::WebDav(WebDavProperty::GetLastModified)
| DavProperty::WebDav(WebDavProperty::ResourceType)
| DavProperty::WebDav(WebDavProperty::LockDiscovery)
| DavProperty::WebDav(WebDavProperty::SupportedLock)
| DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)
| DavProperty::WebDav(WebDavProperty::GetContentLanguage)
| DavProperty::WebDav(WebDavProperty::GetContentLength)
| DavProperty::WebDav(WebDavProperty::GetContentType)
| DavProperty::DeadProperty(_)
)
}
}
impl ReportSet {
pub fn calendar() -> Vec<ReportSet> {
vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
ReportSet::ExpandProperty,
ReportSet::CalendarQuery,
ReportSet::CalendarMultiGet,
ReportSet::FreeBusyQuery,
]
}
pub fn addressbook() -> Vec<ReportSet> {
vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
ReportSet::ExpandProperty,
ReportSet::AddressbookQuery,
ReportSet::AddressbookMultiGet,
]
}
pub fn file() -> Vec<ReportSet> {
vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
]
}
pub fn principal() -> Vec<ReportSet> {
vec![
ReportSet::PrincipalPropertySearch,
ReportSet::PrincipalSearchPropertySet,
ReportSet::PrincipalMatch,
]
}
}
+295
View File
@@ -0,0 +1,295 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
Collation, MatchType,
property::{DavProperty, DavValue, LockScope, LockType},
response::Ace,
};
use crate::{Condition, Depth};
use calcard::{
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::{VCardParameterName, VCardProperty},
};
use types::{
TimeRange,
dead_property::{ArchivedDeadProperty, ArchivedDeadPropertyTag, DeadElementTag, DeadProperty},
};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type", content = "data"))]
pub enum PropFind {
#[default]
PropName,
AllProp(Vec<DavProperty>),
Prop(Vec<DavProperty>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PropertyUpdate {
pub set: Vec<DavPropertyValue>,
pub remove: Vec<DavProperty>,
pub set_first: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct DavPropertyValue {
pub property: DavProperty,
pub value: DavValue,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct MkCol {
pub is_mkcalendar: bool,
pub props: Vec<DavPropertyValue>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct LockInfo {
pub lock_scope: LockScope,
pub lock_type: LockType,
pub owner: Option<DeadProperty>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type"))]
pub enum Report {
AddressbookQuery(AddressbookQuery),
AddressbookMultiGet(MultiGet),
CalendarQuery(CalendarQuery),
CalendarMultiGet(MultiGet),
FreeBusyQuery(FreeBusyQuery),
SyncCollection(SyncCollection),
ExpandProperty(ExpandProperty),
AclPrincipalPropSet(AclPrincipalPropSet),
PrincipalMatch(PrincipalMatch),
PrincipalPropertySearch(PrincipalPropertySearch),
PrincipalSearchPropertySet,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct ExpandProperty {
pub properties: Vec<ExpandPropertyItem>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct ExpandPropertyItem {
pub property: DavProperty,
pub depth: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct AddressbookQuery {
pub properties: PropFind,
pub filters: Vec<Filter<(), VCardPropertyWithGroup, VCardParameterName>>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct VCardPropertyWithGroup {
pub name: VCardProperty,
pub group: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct CalendarQuery {
pub properties: PropFind,
pub filters:
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>,
pub timezone: Timezone,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type"))]
pub enum Timezone {
Name(String),
Id(String),
None,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct FreeBusyQuery {
pub range: Option<TimeRange>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct MultiGet {
pub properties: PropFind,
pub hrefs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct SyncCollection {
pub sync_token: Option<String>,
pub properties: PropFind,
pub depth: Depth,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type"))]
pub enum Filter<A, B, C> {
AnyOf,
AllOf,
Component {
comp: A,
op: FilterOp,
},
Property {
comp: A,
prop: B,
op: FilterOp,
},
Parameter {
comp: A,
prop: B,
param: C,
op: FilterOp,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type", content = "data"))]
pub enum FilterOp {
Exists,
Undefined,
TimeRange(TimeRange),
TextMatch(TextMatch),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(test, serde(tag = "type"))]
pub struct TextMatch {
pub match_type: MatchType,
pub value: String,
pub collation: Collation,
pub negate: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct Acl {
pub aces: Vec<Ace>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct AclPrincipalPropSet {
pub properties: Vec<DavProperty>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PrincipalMatch {
pub principal_properties: PrincipalMatchProperties,
pub properties: Vec<DavProperty>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum PrincipalMatchProperties {
Properties(Vec<DavProperty>),
Self_,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PrincipalPropertySearch {
pub property_search: Vec<PropertySearch>,
pub properties: Vec<DavProperty>,
pub apply_to_principal_collection_set: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PropertySearch {
pub property: DavProperty,
pub match_: String,
}
impl PropertyUpdate {
pub fn has_changes(&self) -> bool {
!self.set.is_empty() || !self.remove.is_empty()
}
}
impl FreeBusyQuery {
pub fn new(start: i64, end: i64) -> Self {
FreeBusyQuery {
range: Some(TimeRange { start, end }),
}
}
}
pub trait DavDeadProperty {
fn to_dav_values(&self, output: &mut Vec<DavPropertyValue>);
}
impl DavDeadProperty for ArchivedDeadProperty {
fn to_dav_values(&self, output: &mut Vec<DavPropertyValue>) {
let mut depth: u32 = 0;
let mut tags = Vec::new();
let mut tag_start = None;
for tag in self.0.iter() {
match tag {
ArchivedDeadPropertyTag::ElementStart(start) => {
if depth == 0 {
tag_start = Some(DeadElementTag::from(start));
} else {
tags.push(tag.into());
}
depth += 1;
}
ArchivedDeadPropertyTag::ElementEnd => {
depth = depth.saturating_sub(1);
if depth > 0 {
tags.push(tag.into());
} else if let Some(tag_start) = tag_start.take() {
output.push(DavPropertyValue::new(
DavProperty::DeadProperty(tag_start),
DavValue::DeadProperty(DeadProperty(std::mem::take(&mut tags))),
));
}
}
ArchivedDeadPropertyTag::Text(_) => {
if tag_start.is_some() {
tags.push(tag.into());
}
}
}
}
}
}
impl Condition<'_> {
pub fn is_none_match(&self) -> bool {
match self {
Condition::ETag { is_not, .. } | Condition::Exists { is_not } => *is_not,
Condition::StateToken { .. } => false,
}
}
}
+430
View File
@@ -0,0 +1,430 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{
Namespaces,
property::{DavProperty, Privilege},
request::{DavPropertyValue, Filter},
};
use calcard::{
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::{VCardParameterName, VCardProperty},
};
use hyper::StatusCode;
use std::{borrow::Cow, fmt::Display};
pub struct MultiStatus {
pub namespaces: Namespaces,
pub response: List<Response>,
pub response_description: Option<ResponseDescription>,
pub sync_token: Option<SyncToken>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct Response {
pub href: Href,
pub typ: ResponseType,
pub error: Option<Condition>,
pub response_description: Option<ResponseDescription>,
pub location: Option<Location>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum ResponseType {
PropStat(List<PropStat>),
Status { href: List<Href>, status: Status },
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct Status(pub StatusCode);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct Location(pub Href);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct ResponseDescription(pub String);
#[repr(transparent)]
pub struct SyncToken(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct Href(pub String);
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct List<T: Display>(pub Vec<T>);
pub struct MkColResponse {
pub namespaces: Namespaces,
pub propstat: List<PropStat>,
pub mkcalendar: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PropStat {
pub prop: Prop,
pub status: Status,
pub error: Option<Condition>,
pub response_description: Option<ResponseDescription>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
#[repr(transparent)]
pub struct Prop(pub List<DavPropertyValue>);
pub struct PropResponse {
pub namespaces: Namespaces,
pub properties: List<DavPropertyValue>,
}
#[derive(Default)]
pub struct ScheduleResponse {
pub items: List<ScheduleResponseItem>,
}
#[derive(Default)]
pub struct ScheduleResponseItem {
pub recipient: Href,
pub request_status: Cow<'static, str>,
pub calendar_data: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct SupportedPrivilege {
pub privilege: Privilege,
pub abstract_: bool,
pub description: String,
pub supported_privilege: List<SupportedPrivilege>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct Ace {
pub principal: Principal,
pub invert: bool,
pub grant_deny: GrantDeny,
pub protected: bool,
pub inherited: Option<Href>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum GrantDeny {
Grant(List<Privilege>),
Deny(List<Privilege>),
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum Principal {
Href(Href),
Response(Response),
All,
#[default]
Authenticated,
Unauthenticated,
Property(List<DavPropertyValue>),
Self_,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct AclRestrictions {
pub grant_only: bool,
pub no_invert: bool,
pub deny_before_grant: bool,
pub required_principal: Option<RequiredPrincipal>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum RequiredPrincipal {
All,
Authenticated,
Unauthenticated,
Self_,
Href(List<Href>),
Property(Vec<DavPropertyValue>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PrincipalSearchPropertySet {
pub namespaces: Namespaces,
pub properties: List<PrincipalSearchProperty>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct PrincipalSearchProperty {
pub name: DavProperty,
pub description: String,
}
pub struct ErrorResponse {
pub namespaces: Namespaces,
pub error: Condition,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum Condition {
Base(BaseCondition),
Cal(CalCondition),
Card(CardCondition),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum BaseCondition {
NoConflictingLock(List<Href>),
LockTokenSubmitted(List<Href>),
LockTokenMatchesRequestUri,
CannotModifyProtectedProperty,
NoExternalEntities,
PreservedLiveProperties,
PropFindFiniteDepth,
ResourceMustBeNull,
NeedPrivileges(List<Resource>),
NoAceConflict,
NoProtectedAceConflict,
NoInheritedAceConflict,
LimitedNumberOfAces,
DenyBeforeGrant,
GrantOnly,
NoInvert,
NoAbstract,
NotSupportedPrivilege,
MissingRequiredPrincipal,
RecognizedPrincipal,
AllowedPrincipal,
NumberOfMatchesWithinLimit,
QuotaNotExceeded,
ValidResourceType,
ValidSyncToken,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub struct Resource {
pub href: Href,
pub privilege: Privilege,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum CalCondition {
CalendarCollectionLocationOk,
ValidCalendarData,
ValidFilter,
ValidCalendarObjectResource,
ValidTimezone,
NoUidConflict(Href),
InitializeCalendarCollection,
SupportedCalendarData,
SupportedFilter(
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>,
),
SupportedCollation(String),
SupportedCalendarComponent,
MinDateTime,
MaxDateTime,
MaxResourceSize(u32),
MaxInstances,
MaxAttendeesPerInstance,
UniqueSchedulingObjectResource(Href),
SameOrganizerInAllComponents,
AllowedOrganizerObjectChange,
AllowedAttendeeObjectChange,
DefaultCalendarNeeded,
ValidScheduleDefaultCalendarUrl,
ValidSchedulingMessage,
ValidOrganizer,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))]
pub enum CardCondition {
SupportedAddressData,
SupportedAddressDataConversion,
SupportedFilter(Vec<Filter<(), VCardProperty, VCardParameterName>>),
SupportedCollation(String),
ValidAddressData,
NoUidConflict(Href),
MaxResourceSize(u32),
AddressBookCollectionLocationOk,
}
impl BaseCondition {
pub fn status(&self) -> StatusCode {
match self {
BaseCondition::NoConflictingLock(_) => StatusCode::LOCKED,
BaseCondition::CannotModifyProtectedProperty => StatusCode::FORBIDDEN,
BaseCondition::LockTokenSubmitted(_) => StatusCode::LOCKED,
BaseCondition::LockTokenMatchesRequestUri => StatusCode::CONFLICT,
BaseCondition::NoExternalEntities => StatusCode::FORBIDDEN,
BaseCondition::PreservedLiveProperties => StatusCode::CONFLICT,
BaseCondition::PropFindFiniteDepth => StatusCode::FORBIDDEN,
BaseCondition::ResourceMustBeNull => StatusCode::CONFLICT,
BaseCondition::NeedPrivileges(_) => StatusCode::FORBIDDEN,
BaseCondition::NumberOfMatchesWithinLimit => StatusCode::FORBIDDEN,
_ => StatusCode::FORBIDDEN,
}
}
}
impl From<String> for Href {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for Href {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
impl MultiStatus {
pub fn is_empty(&self) -> bool {
self.response.0.is_empty()
}
}
impl BaseCondition {
pub fn display_name(&self) -> &'static str {
match self {
BaseCondition::NoConflictingLock(_) => "NoConflictingLock",
BaseCondition::CannotModifyProtectedProperty => "CannotModifyProtectedProperty",
BaseCondition::LockTokenSubmitted(_) => "LockTokenSubmitted",
BaseCondition::LockTokenMatchesRequestUri => "LockTokenMatchesRequestUri",
BaseCondition::NoExternalEntities => "NoExternalEntities",
BaseCondition::PreservedLiveProperties => "PreservedLiveProperties",
BaseCondition::PropFindFiniteDepth => "PropFindFiniteDepth",
BaseCondition::ResourceMustBeNull => "ResourceMustBeNull",
BaseCondition::NeedPrivileges(_) => "NeedPrivileges",
BaseCondition::NoAceConflict => "NoAceConflict",
BaseCondition::NoProtectedAceConflict => "NoProtectedAceConflict",
BaseCondition::NoInheritedAceConflict => "NoInheritedAceConflict",
BaseCondition::LimitedNumberOfAces => "LimitedNumberOfAces",
BaseCondition::DenyBeforeGrant => "DenyBeforeGrant",
BaseCondition::GrantOnly => "GrantOnly",
BaseCondition::NoInvert => "NoInvert",
BaseCondition::NoAbstract => "NoAbstract",
BaseCondition::NotSupportedPrivilege => "NotSupportedPrivilege",
BaseCondition::MissingRequiredPrincipal => "MissingRequiredPrincipal",
BaseCondition::RecognizedPrincipal => "RecognizedPrincipal",
BaseCondition::AllowedPrincipal => "AllowedPrincipal",
BaseCondition::NumberOfMatchesWithinLimit => "NumberOfMatchesWithinLimit",
BaseCondition::QuotaNotExceeded => "QuotaNotExceeded",
BaseCondition::ValidResourceType => "ValidResourceType",
BaseCondition::ValidSyncToken => "ValidSyncToken",
}
}
}
impl CalCondition {
pub fn display_name(&self) -> &'static str {
match self {
CalCondition::CalendarCollectionLocationOk => "CalendarCollectionLocationOk",
CalCondition::ValidCalendarData => "ValidCalendarData",
CalCondition::ValidFilter => "ValidFilter",
CalCondition::ValidCalendarObjectResource => "ValidCalendarObjectResource",
CalCondition::ValidTimezone => "ValidTimezone",
CalCondition::NoUidConflict(_) => "NoUidConflict",
CalCondition::InitializeCalendarCollection => "InitializeCalendarCollection",
CalCondition::SupportedCalendarData => "SupportedCalendarData",
CalCondition::SupportedFilter(_) => "SupportedFilter",
CalCondition::SupportedCollation(_) => "SupportedCollation",
CalCondition::MinDateTime => "MinDateTime",
CalCondition::MaxDateTime => "MaxDateTime",
CalCondition::MaxResourceSize(_) => "MaxResourceSize",
CalCondition::MaxInstances => "MaxInstances",
CalCondition::MaxAttendeesPerInstance => "MaxAttendeesPerInstance",
CalCondition::UniqueSchedulingObjectResource(_) => "UniqueSchedulingObjectResource",
CalCondition::SameOrganizerInAllComponents => "SameOrganizerInAllComponents",
CalCondition::AllowedOrganizerObjectChange => "AllowedOrganizerObjectChange",
CalCondition::AllowedAttendeeObjectChange => "AllowedAttendeeObjectChange",
CalCondition::DefaultCalendarNeeded => "DefaultCalendarNeeded",
CalCondition::ValidScheduleDefaultCalendarUrl => "ValidScheduleDefaultCalendarUrl",
CalCondition::ValidSchedulingMessage => "ValidSchedulingMessage",
CalCondition::ValidOrganizer => "ValidOrganizer",
CalCondition::SupportedCalendarComponent => "SupportedCalendarComponent",
}
}
}
impl CardCondition {
pub fn display_name(&self) -> &'static str {
match self {
CardCondition::SupportedAddressData => "SupportedAddressData",
CardCondition::SupportedAddressDataConversion => "SupportedAddressDataConversion",
CardCondition::SupportedFilter(_) => "SupportedFilter",
CardCondition::SupportedCollation(_) => "SupportedCollation",
CardCondition::ValidAddressData => "ValidAddressData",
CardCondition::NoUidConflict(_) => "NoUidConflict",
CardCondition::MaxResourceSize(_) => "MaxResourceSize",
CardCondition::AddressBookCollectionLocationOk => "AddressBookCollectionLocationOk",
}
}
}
impl Condition {
pub fn display_name(&self) -> &'static str {
match self {
Condition::Base(base) => base.display_name(),
Condition::Cal(cal) => cal.display_name(),
Condition::Card(card) => card.display_name(),
}
}
}
#[cfg(test)]
mod serde_impl {
use super::Status;
use hyper::StatusCode;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
impl Serialize for Status {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Serialize the status code as a u16
serializer.serialize_u16(self.0.as_u16())
}
}
impl<'de> Deserialize<'de> for Status {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize as u16
let status_value = u16::deserialize(deserializer)?;
// Convert u16 to StatusCode
let status_code = StatusCode::try_from(status_value).map_err(|_| {
serde::de::Error::custom(format!("Invalid status code: {}", status_value))
})?;
Ok(Status(status_code))
}
}
}