Skip to content

Commit 9e7ccb5

Browse files
tobixenclaude
andcommitted
feat: add calendar owner example and improve testFindCalendarOwner (closes #544)
- Add examples/calendar_owner_examples.py demonstrating how to get the owner of a calendar via DAV:owner and look up their calendar-user address via Principal.get_vcal_address() - Improve testFindCalendarOwner to actually exercise the owner→principal →get_vcal_address() chain when the server supports both properties - Add test_calendar_owner_examples to tests/test_examples.py - Fix FeatureSet collapse regression: add explicit default to scheduling.mailbox in FEATURES so that _derive_from_subfeatures() does not roll the child quirk status up to the parent when CheckSchedulingDetails has not run Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 289cbe0 commit 9e7ccb5

File tree

4 files changed

+124
-4
lines changed

4 files changed

+124
-4
lines changed

caldav/compatibility_hints.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ class FeatureSet:
271271
"scheduling.mailbox": {
272272
"description": "Server provides schedule-inbox and schedule-outbox collections for the principal (RFC6638 sections 2.1-2.2). When unsupported, calls to schedule_inbox() or schedule_outbox() raise NotFoundError.",
273273
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.1"],
274+
"default": {"support": "full"},
274275
},
275276
"scheduling.calendar-user-address-set": {
276277
"description": "Server provides the calendar-user-address-set property on the principal (RFC6638 section 2.4.1), used to identify a user's email/URI for scheduling purposes. When unsupported, calendar_user_address_set() raises NotFoundError.",
@@ -985,7 +986,7 @@ def dotted_feature_set_list(self, compact=False):
985986
'principal-search': "unsupported",
986987
## Zimbra implements server-side automatic scheduling: invitations are
987988
## auto-processed into the attendee's calendar; no iTIP notification appears in the inbox.
988-
"scheduling": True,
989+
"scheduling.mailbox": True,
989990
"scheduling.mailbox.inbox-delivery": {"support": "unsupported"},
990991

991992
"old_flags": [
@@ -1042,7 +1043,7 @@ def dotted_feature_set_list(self, compact=False):
10421043
#"search.unlimited-time-range": {"support": "broken"},
10431044
## Bedework uses a pre-built Docker image with no easy way to add users, so
10441045
## cross-user scheduling tests cannot be run; inbox-delivery behaviour is unknown.
1045-
"scheduling.mailbox.inbox-delivery": {"support": "unknown"},
1046+
"scheduling.mailbox": {"support": "unknown"},
10461047

10471048
## TODO: play with this and see if it's needed
10481049
'old_flags': [
@@ -1070,6 +1071,7 @@ def dotted_feature_set_list(self, compact=False):
10701071
# Baikal (sabre/dav) delivers iTIP notifications to the attendee inbox AND auto-schedules
10711072
# into their calendar (quirk: both delivery modes happen simultaneously).
10721073
"scheduling.mailbox.inbox-delivery": {"support": "quirk", "behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar"},
1074+
"scheduling.mailbox": True,
10731075
"http.multiplexing": "fragile", ## ref https://github.com/python-caldav/caldav/issues/564
10741076
'search.comp-type.optional': {'support': 'ungraceful'},
10751077
'search.recurrences.expanded.todo': {'support': 'unsupported'},
@@ -1115,6 +1117,7 @@ def dotted_feature_set_list(self, compact=False):
11151117
# AND delivers an iTIP notification copy to the attendee's schedule-inbox.
11161118
# Clients do not need to explicitly accept from the inbox (auto-accept is done),
11171119
# but inbox items do appear. This is "quirk" behaviour: both delivery modes happen.
1120+
"scheduling.mailbox": True,
11181121
"scheduling.mailbox.inbox-delivery": {
11191122
"support": "quirk",
11201123
"behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar",
@@ -1141,6 +1144,7 @@ def dotted_feature_set_list(self, compact=False):
11411144
"http.multiplexing": { "support": "unsupported" },
11421145
# DAViCal delivers iTIP notifications to the attendee inbox AND auto-schedules
11431146
# into their calendar (quirk: both delivery modes happen simultaneously).
1147+
"scheduling.mailbox": True,
11441148
"scheduling.mailbox.inbox-delivery": {"support": "quirk", "behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar"},
11451149
"search.comp-type.optional": { "support": "fragile" },
11461150
"search.recurrences.expanded.exception": { "support": "unsupported" },
@@ -1326,7 +1330,7 @@ def dotted_feature_set_list(self, compact=False):
13261330
davis = {
13271331
# Davis uses sabre/dav (same backend as Baikal): delivers iTIP notifications to the
13281332
# attendee inbox AND auto-schedules into their calendar (quirk behaviour).
1329-
"scheduling": True,
1333+
"scheduling.mailbox": True,
13301334
"scheduling.mailbox.inbox-delivery": {"support": "quirk", "behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar"},
13311335
"search.recurrences.expanded.todo": {"support": "unsupported"},
13321336
"search.recurrences.expanded.exception": {"support": "unsupported"},
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Examples for finding the owner of a calendar and looking up their address.
3+
4+
Use case: when a calendar is shared with you, you may want to know who owns
5+
it and how to reach them.
6+
7+
See also: https://github.com/python-caldav/caldav/issues/544
8+
"""
9+
10+
import sys
11+
12+
sys.path.insert(0, "..")
13+
sys.path.insert(0, ".")
14+
15+
import caldav
16+
from caldav import get_davclient
17+
from caldav.elements import dav
18+
19+
20+
def find_calendar_owner(calendar):
21+
"""
22+
Return the owner URL of a calendar.
23+
24+
Uses the DAV:owner property (WebDAV RFC 4918, section 14.17). The owner
25+
is returned as a URL string pointing to the owner's principal resource.
26+
Returns None if the server does not expose the property.
27+
28+
Args:
29+
calendar: a :class:`caldav.Calendar` object
30+
31+
Returns:
32+
str | None: the owner's principal URL, or None
33+
"""
34+
return calendar.get_property(dav.Owner())
35+
36+
37+
def find_calendar_owner_address(calendar):
38+
"""
39+
Return the calendar-user-address (typically an e-mail URI like
40+
``mailto:user@example.com``) of a calendar's owner.
41+
42+
This is a two-step operation:
43+
44+
1. Fetch the DAV:owner property of the calendar to get the owner's
45+
principal URL.
46+
2. Construct a :class:`caldav.Principal` from that URL and call
47+
:meth:`~caldav.Principal.get_vcal_address` to retrieve the
48+
``calendar-user-address-set`` property (RFC 6638 section 2.4.1).
49+
50+
Requires the server to support both the DAV:owner property and the
51+
``CALDAV:calendar-user-address-set`` principal property. Returns None
52+
when either piece of information is unavailable.
53+
54+
Args:
55+
calendar: a :class:`caldav.Calendar` object
56+
57+
Returns:
58+
icalendar.vCalAddress | None: the owner's calendar address, or None
59+
"""
60+
owner_url = find_calendar_owner(calendar)
61+
if owner_url is None:
62+
return None
63+
64+
owner_principal = caldav.Principal(client=calendar.client, url=owner_url)
65+
try:
66+
return owner_principal.get_vcal_address()
67+
except Exception:
68+
return None
69+
70+
71+
def run_examples():
72+
"""
73+
Run the calendar-owner examples against a live server.
74+
75+
Connects via :func:`caldav.get_davclient` (reads credentials from the
76+
environment or config file), creates a temporary calendar, and
77+
demonstrates how to retrieve its owner URL and calendar-user address.
78+
"""
79+
with get_davclient() as client:
80+
principal = client.principal()
81+
calendar = principal.make_calendar(name="Owner example calendar")
82+
try:
83+
owner_url = find_calendar_owner(calendar)
84+
if owner_url is not None:
85+
print(f"Calendar owner URL: {owner_url}")
86+
87+
owner_address = find_calendar_owner_address(calendar)
88+
if owner_address is not None:
89+
print(f"Calendar owner address: {owner_address}")
90+
else:
91+
print(
92+
"Calendar owner address: not available (server may not support calendar-user-address-set)"
93+
)
94+
else:
95+
print("DAV:owner property not exposed by this server")
96+
finally:
97+
calendar.delete()
98+
99+
100+
if __name__ == "__main__":
101+
run_examples()

tests/test_caldav.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,17 @@ def testSchedulingMailboxes(self):
11981198
def testFindCalendarOwner(self):
11991199
cal = self._fixCalendar()
12001200
owner = cal.get_property(dav.Owner())
1201-
## TODO: something should probably be asserted about the Owner
1201+
## Not all servers expose the DAV:owner property; None is acceptable.
1202+
if owner is None:
1203+
return
1204+
1205+
## The owner URL should point to a principal resource.
1206+
## Constructing a Principal from it and fetching the vcal address
1207+
## demonstrates the full workflow from issue #544.
1208+
if self.is_supported("scheduling.calendar-user-address-set"):
1209+
owner_principal = Principal(client=self.caldav, url=owner)
1210+
address = owner_principal.get_vcal_address()
1211+
assert address is not None
12021212

12031213
def testIssue397(self):
12041214
self.skip_unless_support("save-load.event.recurrences.exception")

tests/test_examples.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,8 @@ def test_collation(self):
5656

5757
def test_rfc8764_test_conf(self):
5858
pass
59+
60+
def test_calendar_owner_examples(self):
61+
from examples import calendar_owner_examples
62+
63+
calendar_owner_examples.run_examples()

0 commit comments

Comments
 (0)