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
+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>")
}
}