From 10c750f6fa46fb5d5525c7475b5393ed27c837f8 Mon Sep 17 00:00:00 2001 From: Georgiy Date: Sun, 25 Jan 2026 18:21:22 +0300 Subject: [PATCH 1/2] (#1966) overridden recurrence validation --- radicale/app/report.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/radicale/app/report.py b/radicale/app/report.py index 752d04a7..5d41b81b 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -632,6 +632,23 @@ def _split_overridden_vevents( vevent_recurrence = vevent if vevent_recurrence: + # RFC 5545 3.8.4.4: RECURRENCE-ID MUST have the same value type as DTSTART + # in the recurring component. + base_dtstart = vevent_recurrence.dtstart.value + expected_type = datetime.date + if isinstance(base_dtstart, datetime.datetime): + expected_type = datetime.datetime + + for ov in vevents_overridden: + rid = ov.recurrence_id.value + if not isinstance(rid, expected_type): + raise ValueError( + f"component with UID {vevent_recurrence.uid} has " + f"incompatible RECURRENCE-ID value type: base DTSTART is " + f"{type(base_dtstart).__name__}, but override RECURRENCE-ID is " + f"{type(rid).__name__} ({ov.recurrence_id.value!r})" + ) + return ( vevent_recurrence, sorted( vevents_overridden, From 0493a881904c2c40ed339f0c7745e43a697142e9 Mon Sep 17 00:00:00 2001 From: Georgiy Date: Thu, 5 Feb 2026 19:58:32 +0300 Subject: [PATCH 2/2] (#1966) Test for incompatible overridding RECURRENCE-ID dt type --- .../event_mixed_recurrence_id_dt_type.ics | 20 ++++++++++ radicale/tests/test_expand.py | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 radicale/tests/static/event_mixed_recurrence_id_dt_type.ics diff --git a/radicale/tests/static/event_mixed_recurrence_id_dt_type.ics b/radicale/tests/static/event_mixed_recurrence_id_dt_type.ics new file mode 100644 index 00000000..8c9e9edf --- /dev/null +++ b/radicale/tests/static/event_mixed_recurrence_id_dt_type.ics @@ -0,0 +1,20 @@ +BEGIN:VCALENDAR +VERSION:2.0 + +BEGIN:VEVENT +DTSTART:20060102T120000Z +DURATION:PT1H +RRULE:FREQ=DAILY;COUNT=3 +SUMMARY:Recurring event +UID:event_mixed_recurrence_id_dt_type +END:VEVENT + +BEGIN:VEVENT +DTSTART:20060103T140000Z +DURATION:PT1H +RECURRENCE-ID;VALUE=DATE:20060103 +SUMMARY:Override with wrong type +UID:event_mixed_recurrence_id_dt_type +END:VEVENT + +END:VCALENDAR \ No newline at end of file diff --git a/radicale/tests/test_expand.py b/radicale/tests/test_expand.py index 9783abeb..9a1f7369 100644 --- a/radicale/tests/test_expand.py +++ b/radicale/tests/test_expand.py @@ -698,3 +698,43 @@ permissions: RrWw""") status, responses = self.report("/test", request) assert status == 207 assert len(responses) == 0 + + def test_report_with_incompatible_recurrence_id_dt_types(self, caplog) -> None: + """Test report with incompatible RECURRENCE-ID value types (RFC 5545 3.8.4.4)""" + + import logging + caplog.set_level(logging.WARNING) + + event_id = "event_mixed_recurrence_id_dt_type" + start = "20060103T000000Z" + end = "20060105T000000Z" + + self.put("/calendar.ics/", get_file_content(f"{event_id}.ics")) + + request = f""" + + + + + + + + + + + + + + + """ + + status, _, _ = self.request("REPORT", "/calendar.ics/", request) + + assert status == 400 + + logs = caplog.messages + error_logs = [log for log in logs if "incompatible RECURRENCE-ID value type" in log] + assert len(error_logs) > 0 + assert event_id in error_logs[0] + assert "base DTSTART is datetime" in error_logs[0] + assert "override RECURRENCE-ID is date" in error_logs[0]