diff --git a/radicale/app/report.py b/radicale/app/report.py index 2bad1924..821d1e60 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -641,6 +641,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, 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]