From 74d21f011c89398fc6f2b976a764ff86e3f4e61c Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 22 Aug 2025 07:49:09 +0200 Subject: [PATCH 1/5] enrich for optional tzinfo --- radicale/item/filter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/item/filter.py b/radicale/item/filter.py index b846023a..ef43dbcc 100644 --- a/radicale/item/filter.py +++ b/radicale/item/filter.py @@ -47,7 +47,7 @@ else: TRIGGER = datetime | None -def date_to_datetime(d: date) -> datetime: +def date_to_datetime(d: date, tzinfo=vobject.icalendar.utc) -> datetime: """Transform any date to a UTC datetime. If ``d`` is a datetime without timezone, return as UTC datetime. If ``d`` @@ -58,7 +58,7 @@ def date_to_datetime(d: date) -> datetime: d = datetime.combine(d, datetime.min.time()) if not d.tzinfo: # NOTE: using vobject's UTC as it wasn't playing well with datetime's. - d = d.replace(tzinfo=vobject.icalendar.utc) + d = d.replace(tzinfo=tzinfo) return d From e1b19f1a2227b5713e8437b0e7760d4ffa023c2b Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 22 Aug 2025 07:49:54 +0200 Subject: [PATCH 2/5] catch items having tzinfo only on dtstart or dtend set for whatever reason, overtake tzinfo from the other one --- radicale/item/filter.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/radicale/item/filter.py b/radicale/item/filter.py index ef43dbcc..94cdc015 100644 --- a/radicale/item/filter.py +++ b/radicale/item/filter.py @@ -366,6 +366,21 @@ def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str, dtend = getattr(child, "dtend", None) if dtend is not None: dtend = dtend.value + + # Ensure that both datetime.datetime objects have a timezone or + # both do not have one before doing calculations. This is required + # as the library does not support performing mathematical operations + # on timezone-aware and timezone-naive objects. See #1847 + if hasattr(dtstart, 'tzinfo') and hasattr(dtend, 'tzinfo'): + if dtstart.tzinfo is None and dtend.tzinfo is not None: + dtstart_orig = dtstart + dtstart = date_to_datetime(dtstart, dtend.astimezone().tzinfo) + logger.debug("TRACE/ITEM/FILTER/get_children: overtake missing tzinfo on dtstart from dtend: '%s' -> '%s'", dtstart_orig, dtstart) + elif dtstart.tzinfo is not None and dtend.tzinfo is None: + dtend_orig = dtend + dtend = date_to_datetime(dtend, dtstart.astimezone().tzinfo) + logger.debug("TRACE/ITEM/FILTER/get_children: overtake missing tzinfo on dtend from dtstart: '%s' -> '%s'", dtend_orig, dtend) + original_duration = (dtend - dtstart).total_seconds() dtend = date_to_datetime(dtend) From 2a808fd37391de4e4092444f0e22db11b5a00225 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 22 Aug 2025 07:50:47 +0200 Subject: [PATCH 3/5] test items having tzinfo only on dtstart or dtend set for whatever reason --- radicale/tests/static/event_issue1847_1.ics | 14 ++++++++++++++ radicale/tests/static/event_issue1847_2.ics | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 radicale/tests/static/event_issue1847_1.ics create mode 100644 radicale/tests/static/event_issue1847_2.ics diff --git a/radicale/tests/static/event_issue1847_1.ics b/radicale/tests/static/event_issue1847_1.ics new file mode 100644 index 00000000..121c0c6c --- /dev/null +++ b/radicale/tests/static/event_issue1847_1.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//algoo.fr//NONSGML Open Calendar v0.9//EN +BEGIN:VEVENT +CREATED:20250814T153429Z +LAST-MODIFIED:20250814T153503Z +DTSTAMP:20250814T153503Z +UID:f91964cb-53ca-4942-8811-c38f076f4328 +SUMMARY:error +DTSTART:20250814T180000 +DTEND;TZID=Europe/Brussels:20250814T190000 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/radicale/tests/static/event_issue1847_2.ics b/radicale/tests/static/event_issue1847_2.ics new file mode 100644 index 00000000..03d09b49 --- /dev/null +++ b/radicale/tests/static/event_issue1847_2.ics @@ -0,0 +1,14 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//algoo.fr//NONSGML Open Calendar v0.9//EN +BEGIN:VEVENT +CREATED:20250814T153429Z +LAST-MODIFIED:20250814T153503Z +DTSTAMP:20250814T153503Z +UID:f91964cb-53ca-4942-8811-c38f076f4328 +SUMMARY:error +DTSTART;TZID=Europe/Brussels:20250814T180000 +DTEND:20250814T190000 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR From 7f28f69452d3c6250d7efdad85eb38464016b7a8 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 22 Aug 2025 07:51:15 +0200 Subject: [PATCH 4/5] extend test for items having tzinfo only on dtstart or dtend set for whatever reason, overtake tzinfo from the other one --- radicale/tests/test_base.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index eb25bd1f..a9d0acc7 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -306,6 +306,22 @@ permissions: RrWw""") for uid2 in uids[i + 1:]: assert uid1 != uid2 + def test_add_event_tz_dtend_only(self) -> None: + """Add an event having TZ only on DTEND.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_issue1847_1.ics") + path = "/calendar.ics/event_issue1847_1.ics" + self.put(path, event) + _, headers, answer = self.request("GET", path, check=200) + + def test_add_event_tz_dtstart_only(self) -> None: + """Add an event having TZ only on DTSTART.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_issue1847_2.ics") + path = "/calendar.ics/event_issue1847_2.ics" + self.put(path, event) + _, headers, answer = self.request("GET", path, check=200) + def test_verify(self) -> None: """Verify the storage.""" contacts = get_file_content("contact_multiple.vcf") From 699a996be4a77240422421c2d53e5f5afd7f9e26 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 22 Aug 2025 07:51:54 +0200 Subject: [PATCH 5/5] changelog for items having tzinfo only on dtstart or dtend set for whatever reason, overtake tzinfo from the other one --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eafbee6..a144f1db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Add: [hook] dryrun: option to disable real hook action for testing, add tests for email+rabbitmq * Fix: storage hook path now added to DELETE, MKCOL, MKCALENDAR, MOVE, and PROPPATCH * Add: storage hook placeholder now supports "request" and "to_path" (MOVE only) +* Improve: catch items having tzinfo only on dtstart or dtend set for whatever reason, overtake tzinfo from the other one ## 3.5.4 * Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute)