Skip to content

Commit 289cbe0

Browse files
committed
test: implement RFC6638 testInviteAndRespond with per-server fixes
Implement testInviteAndRespond integration test covering invite/accept workflows (RFC6638), with support for both auto-scheduling and inbox-delivery modes, and per-server fixes for all supported servers: - Handle auto-scheduling servers (Cyrus, Nextcloud) that deliver to the calendar directly without requiring inbox delivery - Restructure test to properly handle both scheduling modes - Cyrus: fix virtdomains config for cross-user scheduling - Nextcloud, DAViCal, Baikal, Zimbra, SOGo, Davis, CCS, Stalwart: fixes - Add scheduling.mailbox.inbox-delivery compatibility hints per server - Rename scheduling.inbox-delivery → scheduling.mailbox.inbox-delivery - Fix wrong RFC6638 section references in compatibility hints - Pass scheduling_users as extra_clients in testCheckCompatibility
1 parent 3e182a1 commit 289cbe0

File tree

14 files changed

+405
-99
lines changed

14 files changed

+405
-99
lines changed

caldav/calendarobjectresource.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ def is_invite_request(self) -> bool:
696696
def is_invite_reply(self) -> bool:
697697
"""
698698
Returns True if the object is a reply, see
699-
:rfc:`2446#section-3.2.3`.
699+
:rfc:`5546#section-3.2`.
700700
"""
701701
self.load(only_if_unloaded=True)
702702
return self.icalendar_instance.get("method", None) == "REPLY"

caldav/compatibility_hints.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,13 +269,19 @@ class FeatureSet:
269269
"links": ["https://datatracker.ietf.org/doc/html/rfc6638"],
270270
},
271271
"scheduling.mailbox": {
272-
"description": "Server provides schedule-inbox and schedule-outbox collections for the principal (RFC6638 sections 2.2-2.3). When unsupported, calls to schedule_inbox() or schedule_outbox() raise NotFoundError.",
273-
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.2"],
272+
"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.",
273+
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.1"],
274274
},
275275
"scheduling.calendar-user-address-set": {
276276
"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.",
277277
"links": ["https://datatracker.ietf.org/doc/html/rfc6638#section-2.4.1"],
278278
},
279+
"scheduling.mailbox.inbox-delivery": {
280+
"description": "Server delivers incoming scheduling REQUEST messages to the attendee's schedule-inbox (RFC6638 section 4.1). When unsupported, the server implements automatic scheduling: invitations are auto-processed and placed directly on the attendee's calendar without appearing in the inbox. Clients should check this feature to know whether to look for inbox items after sending an invite, or check the attendee calendar directly.",
281+
"links": [
282+
"https://datatracker.ietf.org/doc/html/rfc6638#section-4.1",
283+
],
284+
},
279285
'freebusy-query': {'description': "freebusy queries come in two flavors, one query can be done towards a CalDAV server as defined in RFC4791, another query can be done through the scheduling framework, RFC 6638. Only RFC4791 is tested for as today"},
280286
"freebusy-query.rfc4791": {
281287
"description": "Server supports free/busy-query REPORT as specified in RFC4791 section 7.10. The REPORT allows clients to query for free/busy time information for a time range. Servers without this support will typically return an error (often 500 Internal Server Error or 501 Not Implemented). Note: RFC6638 defines a different freebusy mechanism for scheduling",
@@ -977,6 +983,10 @@ def dotted_feature_set_list(self, compact=False):
977983
'search.comp-type.optional': {'support': 'fragile'}, ## TODO: more research on this, looks like a bug in the checker,
978984
'search.time-range.alarm': {'support': 'unsupported'},
979985
'principal-search': "unsupported",
986+
## Zimbra implements server-side automatic scheduling: invitations are
987+
## auto-processed into the attendee's calendar; no iTIP notification appears in the inbox.
988+
"scheduling": True,
989+
"scheduling.mailbox.inbox-delivery": {"support": "unsupported"},
980990

981991
"old_flags": [
982992
## apparently, zimbra has no journal support
@@ -1004,7 +1014,7 @@ def dotted_feature_set_list(self, compact=False):
10041014

10051015
bedework = {
10061016
## If tests are yielding unexpected results, try to increase this:
1007-
'search-cache': {'behaviour': 'delay', 'delay': 1.5},
1017+
'search-cache': {'behaviour': 'delay', 'delay': 3},
10081018

10091019
'test-calendar': {'cleanup-regime': 'wipe-calendar'},
10101020
'auto-connect.url': {'basepath': '/ucaldav/'},
@@ -1024,7 +1034,15 @@ def dotted_feature_set_list(self, compact=False):
10241034
'search.comp-type.optional': {'support': 'ungraceful'},
10251035
'search.is-not-defined.dtend': False,
10261036
"principal-search": { "support": "ungraceful" },
1027-
"search.unlimited-time-range": {"support": "broken"},
1037+
## Bedework hides past non-recurring events from REPORT without a time-range filter,
1038+
## but still returns recurring events that have future occurrences. The unlimited-time-range
1039+
## check probe is a past-only non-recurring event; it is not returned even though the
1040+
## PrepareCalendar recurring event (RRULE:FREQ=MONTHLY since 2000) is returned.
1041+
## Result: objects is non-empty but the probe event is absent → "broken".
1042+
#"search.unlimited-time-range": {"support": "broken"},
1043+
## Bedework uses a pre-built Docker image with no easy way to add users, so
1044+
## cross-user scheduling tests cannot be run; inbox-delivery behaviour is unknown.
1045+
"scheduling.mailbox.inbox-delivery": {"support": "unknown"},
10281046

10291047
## TODO: play with this and see if it's needed
10301048
'old_flags': [
@@ -1049,6 +1067,9 @@ def dotted_feature_set_list(self, compact=False):
10491067
}
10501068

10511069
baikal = { ## version 0.10.1
1070+
# Baikal (sabre/dav) delivers iTIP notifications to the attendee inbox AND auto-schedules
1071+
# into their calendar (quirk: both delivery modes happen simultaneously).
1072+
"scheduling.mailbox.inbox-delivery": {"support": "quirk", "behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar"},
10521073
"http.multiplexing": "fragile", ## ref https://github.com/python-caldav/caldav/issues/564
10531074
'search.comp-type.optional': {'support': 'ungraceful'},
10541075
'search.recurrences.expanded.todo': {'support': 'unsupported'},
@@ -1089,6 +1110,15 @@ def dotted_feature_set_list(self, compact=False):
10891110
'support': 'fragile',
10901111
'behaviour': 'Deleting a recently created calendar fails'},
10911112
# Cyrus may not properly reject wrong passwords in some configurations
1113+
# Cyrus implements server-side automatic scheduling: for cross-user
1114+
# invites, the server both auto-processes the invite into the attendee's calendar
1115+
# AND delivers an iTIP notification copy to the attendee's schedule-inbox.
1116+
# Clients do not need to explicitly accept from the inbox (auto-accept is done),
1117+
# but inbox items do appear. This is "quirk" behaviour: both delivery modes happen.
1118+
"scheduling.mailbox.inbox-delivery": {
1119+
"support": "quirk",
1120+
"behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar",
1121+
},
10921122
'old_flags': []
10931123
}
10941124

@@ -1109,6 +1139,9 @@ def dotted_feature_set_list(self, compact=False):
11091139
# Disable HTTP/2 multiplexing - davical doesn't support it well and niquests
11101140
# lazy responses cause MultiplexingError when accessing status_code
11111141
"http.multiplexing": { "support": "unsupported" },
1142+
# DAViCal delivers iTIP notifications to the attendee inbox AND auto-schedules
1143+
# into their calendar (quirk: both delivery modes happen simultaneously).
1144+
"scheduling.mailbox.inbox-delivery": {"support": "quirk", "behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar"},
11121145
"search.comp-type.optional": { "support": "fragile" },
11131146
"search.recurrences.expanded.exception": { "support": "unsupported" },
11141147
"search.time-range.alarm": { "support": "unsupported" },
@@ -1128,6 +1161,8 @@ def dotted_feature_set_list(self, compact=False):
11281161
}
11291162

11301163
sogo = {
1164+
## scheduling.mailbox.inbox-delivery behaviour unknown until cross-user scheduling tests run
1165+
"scheduling.mailbox.inbox-delivery": {"support": "unknown"},
11311166
## I'm surprised, I'm quite sure this was passing earlier. reported unsupported with caldav commit a98d50490b872e9b9d8e93e2e401c936ad193003, caldav server checker commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad 2026-02-15
11321167
"search.text.category": False,
11331168
"search.time-range.event.old-dates": False,
@@ -1289,6 +1324,10 @@ def dotted_feature_set_list(self, compact=False):
12891324
## Davis uses sabre/dav (same backend as Baikal), so hints are similar.
12901325
## TODO: consolidate, make a sabredav dict and let davis/baikal build on it
12911326
davis = {
1327+
# Davis uses sabre/dav (same backend as Baikal): delivers iTIP notifications to the
1328+
# attendee inbox AND auto-schedules into their calendar (quirk behaviour).
1329+
"scheduling": True,
1330+
"scheduling.mailbox.inbox-delivery": {"support": "quirk", "behaviour": "server delivers iTIP notification to inbox AND auto-schedules into calendar"},
12921331
"search.recurrences.expanded.todo": {"support": "unsupported"},
12931332
"search.recurrences.expanded.exception": {"support": "unsupported"},
12941333
"search.recurrences.includes-implicit.todo": {"support": "unsupported"},
@@ -1310,6 +1349,8 @@ def dotted_feature_set_list(self, compact=False):
13101349
## cannot be changed. The pre-provisioned "tasks" calendar supports VTODO only.
13111350
## VJOURNAL is not supported at all.
13121351
ccs = {
1352+
## scheduling.mailbox.inbox-delivery behaviour unknown until cross-user scheduling tests run
1353+
"scheduling.mailbox.inbox-delivery": {"support": "unknown"},
13131354
"save-load.journal": {"support": "unsupported"},
13141355
"save-load.todo.mixed-calendar": {"support": "unsupported"},
13151356
# CCS enforces unique UIDs across ALL calendars for a user
@@ -1343,6 +1384,8 @@ def dotted_feature_set_list(self, compact=False):
13431384
## CalDAV served at /dav/cal/<username>/ over HTTP on port 8080.
13441385
## Feature support mostly unknown until tested; starting with empty hints.
13451386
stalwart = {
1387+
## scheduling.mailbox.inbox-delivery behaviour unknown until cross-user scheduling tests run
1388+
"scheduling.mailbox.inbox-delivery": {"support": "unknown"},
13461389
'rate-limit': {
13471390
'enable': True,
13481391
'default_sleep': 3,
@@ -1499,6 +1542,8 @@ def dotted_feature_set_list(self, compact=False):
14991542
'old_flags': [
15001543
'no_relships',
15011544
],
1545+
## OX App Suite has complex user provisioning; cross-user scheduling tests not yet set up.
1546+
"scheduling.mailbox.inbox-delivery": {"support": "unknown"},
15021547
}
15031548

15041549
# fmt: on

tests/docker-test-servers/baikal/create_baikal_db.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,9 @@ def add_baikal_user(db_path: Path, username: str, password: str) -> None:
241241
conn = sqlite3.connect(str(db_path))
242242
cursor = conn.cursor()
243243

244-
cursor.execute("INSERT OR REPLACE INTO users (username, digesta1) VALUES (?, ?)", (username, ha1))
244+
cursor.execute(
245+
"INSERT OR REPLACE INTO users (username, digesta1) VALUES (?, ?)", (username, ha1)
246+
)
245247

246248
cursor.execute(
247249
"INSERT OR IGNORE INTO principals (uri, email, displayname) VALUES (?, ?, ?)",

tests/docker-test-servers/baikal/setup_baikal.sh

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,60 @@
11
#!/bin/bash
2-
# Setup script for Baikal CalDAV server for testing
2+
# Setup script for Baikal CalDAV server for testing.
33
#
4-
# This script helps configure a fresh Baikal installation for testing.
5-
# It can be used both locally and in CI environments.
4+
# Baikal is pre-configured via the committed Specific/ directory which contains
5+
# a pre-seeded db.sqlite with testuser and user1-user3 for scheduling tests.
6+
# This script verifies connectivity and re-seeds users if needed (e.g. if the
7+
# DB was replaced or users were deleted).
68

79
set -e
810

11+
CONTAINER_NAME="baikal-test"
912
BAIKAL_URL="${BAIKAL_URL:-http://localhost:8800}"
10-
BAIKAL_ADMIN_PASSWORD="${BAIKAL_ADMIN_PASSWORD:-admin}"
11-
BAIKAL_USERNAME="${BAIKAL_USERNAME:-testuser}"
12-
BAIKAL_PASSWORD="${BAIKAL_PASSWORD:-testpass}"
13+
DB_PATH="/var/www/baikal/Specific/db/db.sqlite"
14+
REALM="BaikalDAV"
1315

14-
echo "Setting up Baikal CalDAV server at $BAIKAL_URL"
15-
16-
# Wait for Baikal to be ready
1716
echo "Waiting for Baikal to be ready..."
18-
timeout 60 bash -c "until curl -f $BAIKAL_URL/ 2>/dev/null; do echo 'Waiting...'; sleep 2; done" || {
19-
echo "Error: Baikal did not become ready in time"
20-
exit 1
21-
}
17+
max_attempts=30
18+
for i in $(seq 1 $max_attempts); do
19+
if curl -sf "$BAIKAL_URL/" -o /dev/null 2>/dev/null; then
20+
echo "✓ Baikal is ready"
21+
break
22+
fi
23+
if [ $i -eq $max_attempts ]; then
24+
echo "✗ Baikal did not become ready in time"
25+
exit 1
26+
fi
27+
echo -n "."
28+
sleep 2
29+
done
2230

23-
echo "Baikal is ready!"
31+
echo ""
32+
echo "Seeding test users (idempotent)..."
2433

25-
# Note: Baikal requires initial configuration through web interface or config files
26-
# For automated testing, you may need to:
27-
# 1. Pre-configure Baikal by mounting a pre-configured config directory
28-
# 2. Use the Baikal API if available
29-
# 3. Manually configure once and export the config
34+
add_user() {
35+
local username="$1"
36+
local password="$2"
37+
local email="$3"
38+
local displayname="$4"
39+
local ha1
40+
ha1=$(python3 -c "import hashlib; print(hashlib.md5('${username}:${REALM}:${password}'.encode()).hexdigest())")
41+
docker exec "$CONTAINER_NAME" sqlite3 "$DB_PATH" \
42+
"INSERT OR IGNORE INTO users (username, digesta1) VALUES ('${username}', '${ha1}');
43+
INSERT OR IGNORE INTO principals (uri, email, displayname) VALUES ('principals/${username}', '${email}', '${displayname}');"
44+
echo " ${username}: OK"
45+
}
46+
47+
add_user "testuser" "testpass" "testuser@example.com" "Test User"
48+
add_user "user1" "testpass1" "user1@example.com" "User 1"
49+
add_user "user2" "testpass2" "user2@example.com" "User 2"
50+
add_user "user3" "testpass3" "user3@example.com" "User 3"
3051

3152
echo ""
32-
echo "================================================================"
33-
echo "IMPORTANT: Baikal Initial Configuration Required"
34-
echo "================================================================"
35-
echo ""
36-
echo "Baikal requires initial setup through the web interface or"
37-
echo "by providing pre-configured files."
38-
echo ""
39-
echo "For automated testing, you have several options:"
40-
echo ""
41-
echo "1. Access $BAIKAL_URL in your browser and complete the setup"
42-
echo " - Set admin password to: $BAIKAL_ADMIN_PASSWORD"
43-
echo " - Create a test user: $BAIKAL_USERNAME / $BAIKAL_PASSWORD"
44-
echo ""
45-
echo "2. Mount a pre-configured Baikal config directory:"
46-
echo " - Configure Baikal once"
47-
echo " - Export the config directory"
48-
echo " - Mount it in docker-compose.yml or CI"
53+
echo "✓ Baikal setup complete!"
4954
echo ""
50-
echo "3. For CI/CD: See tests/baikal-config/ for sample configs"
55+
echo "Credentials:"
56+
echo " Test user: testuser / testpass"
57+
echo " Scheduling users: user1/testpass1, user2/testpass2, user3/testpass3"
58+
echo " CalDAV URL: $BAIKAL_URL/dav.php"
59+
echo " Auth type: Digest (realm: $REALM)"
5160
echo ""
52-
echo "================================================================"

tests/docker-test-servers/cyrus/docker-compose.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ services:
1111
environment:
1212
- DEFAULTDOMAIN=example.com
1313
- SERVERNAME=cyrus-test
14-
# No volumes - let data be ephemeral for testing
14+
volumes:
15+
# Override the imapd.conf template to set virtdomains: off.
16+
# This fixes iTIP scheduling delivery: with virtdomains: userid the
17+
# caladdress_lookup() function preserves user2@example.com as the
18+
# userid, but mailbox ACLs use the short form user2, causing 403.
19+
- ./imapd.conf:/srv/cyrus-docker-test-server.git/imapd.conf:ro
20+
# Other data remains ephemeral for testing
1521
# This ensures each start is fresh with newly created users
1622
healthcheck:
1723
test: ["CMD", "curl", "-s", "http://localhost:8080/"]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
admins: admin
2+
allowplaintext: yes
3+
altnamespace: no
4+
auditlog: yes
5+
caldav_allowattach: yes
6+
caldav_create_attach: yes
7+
caldav_realm: test
8+
configdirectory: /var/imap/config
9+
conversations_counted_flags: \Draft \Flagged
10+
conversations_max_thread: 1000
11+
conversations: yes
12+
crossdomains: yes
13+
debug: yes
14+
defaultacl: admin lrswipkxtecdan
15+
defaultdomain: {{DEFAULTDOMAIN}}
16+
defaultpartition: default
17+
defaultsearchtier: t1
18+
delete_mode: delayed
19+
expunge_mode: delayed
20+
httpmodules: caldav carddav jmap
21+
httpallowcompress: no
22+
httpprettytelemetry: yes
23+
imipnotifier: imip
24+
improved_mboxlist_sort: yes
25+
jmapauth_allowsasl: yes
26+
jmap_max_calls_in_request: 256
27+
jmap_max_size_request: 51200
28+
jmap_max_size_upload: 209715200
29+
jmap_nonstandard_extensions: yes
30+
jmap_preview_annot: /shared/vendor/messagingengine.com/preview
31+
jmap_querycache_max_age: 5m
32+
jmap_set_has_attachment: no
33+
# this is buggy, turn it off
34+
jmap_vacation: no
35+
maxheaderlines: 4096
36+
maxmessagesize: 209715200
37+
maxquoted: 8388608
38+
maxword: 8388608
39+
partition-default: /var/imap/spool
40+
reverseacls: yes
41+
rfc3028_strict: no
42+
sasl_mech_list: PLAIN LOGIN
43+
sasl_pwcheck_method: saslauthd
44+
sasl_saslauthd_path: /var/run/cyrus/saslauthd.sock
45+
savedate: yes
46+
search_engine: xapian
47+
search_index_headers: no
48+
search_batchsize: 512
49+
search_fuzzy_always: yes
50+
search_maxtime: 30
51+
search_snippet_length: 160
52+
search_normalisation_max: 20000
53+
search_index_language: yes
54+
servername: {{SERVERNAME}}
55+
sievedir: /var/imap/sieve
56+
smtp_backend: host
57+
smtp_host: localhost:25
58+
sync_log: yes
59+
sync_log_channels: squatter
60+
t1searchpartition-default: /var/imap/search
61+
unixhierarchysep: no
62+
# virtdomains: userid is intentionally disabled here.
63+
# With virtdomains: userid, caladdress_lookup() preserves the full email
64+
# form (user2@example.com) as sparam->userid. But mailbox ACLs and
65+
# mbname_userid() use the short form (user2) for default-domain users.
66+
# This mismatch causes caldav_store_preprocess() to fail the ACL check
67+
# with 403 Forbidden when delivering iTIP invites to the attendee's calendar.
68+
# Setting virtdomains: off makes caladdress_lookup() strip the domain for
69+
# local users, so the userid matches the ACL entry.
70+
virtdomains: off

tests/docker-test-servers/davical/setup_davical.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,14 @@ create_user() {
4949
if [ -n "$EXISTING_PRINCIPAL" ]; then
5050
echo "Principal for '${username}' already exists, skipping"
5151
else
52-
run_sql "INSERT INTO principal (type_id, user_no, displayname) SELECT 1, user_no, fullname FROM usr WHERE username='${username}'"
52+
# default_privileges: schedule-deliver (7168 = bits for schedule-deliver-invite +
53+
# schedule-deliver-reply + schedule-query-freebusy) so other users can send
54+
# scheduling invites/replies to this principal's inbox.
55+
run_sql "INSERT INTO principal (type_id, user_no, displayname, default_privileges) SELECT 1, user_no, fullname, 7168::BIT(24) FROM usr WHERE username='${username}'"
5356
echo "Principal for '${username}' created"
5457
fi
58+
# Ensure default_privileges is set even for existing principals (idempotent update)
59+
run_sql "UPDATE principal SET default_privileges = 7168::BIT(24) FROM usr WHERE principal.user_no = usr.user_no AND usr.username = '${username}' AND (default_privileges IS NULL OR default_privileges = 0::BIT(24))"
5560
}
5661

5762
echo ""
@@ -61,6 +66,10 @@ create_user "user1" "testpass1" "User One"
6166
create_user "user2" "testpass2" "User Two"
6267
create_user "user3" "testpass3" "User Three"
6368

69+
echo ""
70+
echo "Enabling local scheduling in DAViCal config..."
71+
docker exec "$DAVICAL_CONTAINER" sed -i 's|// \$c->enable_scheduling = true;|\$c->enable_scheduling = true;|' /etc/davical/config.php || true
72+
6473
echo ""
6574
echo "Verifying CalDAV access..."
6675
max_caldav_attempts=10

tests/docker-test-servers/davis/docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ services:
1717
- AUTH_METHOD=Basic
1818
- APP_SECRET=testserversecret123
1919
- INVITE_FROM_ADDRESS=test@example.com
20+
# Disable SMTP: without a null mailer, sabre/dav tries to send email
21+
# notifications on every PUT/DELETE of a scheduled event and returns 500
22+
# when it cannot connect to the SMTP server.
23+
- MAILER_DSN=null://null
2024
tmpfs:
2125
- /data:size=100m
2226
healthcheck:

0 commit comments

Comments
 (0)