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:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
]))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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("<")?,
|
||||
'>' => out.write_str(">")?,
|
||||
'&' => out.write_str("&")?,
|
||||
'"' => out.write_str(""")?,
|
||||
'\'' => out.write_str("'")?,
|
||||
_ => 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
|
||||
}
|
||||
}
|
||||
@@ -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>")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user