diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcdc6ca..8116d2f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Add: [sharing] conversion_bday_categories (customize) * Add: [sharing] conversion_bday_age_max (limit in case of "age" placeholder is used which blocks using RRULE) * Extension: [sharing/bday conversion]: add STATUS + CLASS fields +* Improve: sanitize item align timezone (add/remove) of EXDATE or RDATE with DTSTART ## 3.7.4 * Fix: sharing: PROPFIND returns now empty owner element in case of a mapped share as clients try PROPFIND on this not accessable href diff --git a/radicale/item/__init__.py b/radicale/item/__init__.py index 14d563c7..4a43f787 100644 --- a/radicale/item/__init__.py +++ b/radicale/item/__init__.py @@ -174,12 +174,26 @@ def check_and_sanitize_items( # EXDATE has value DATE even if DTSTART/DTEND is DATE-TIME. # The RFC is vaguely formulated on the issue. # To resolve the issue convert EXDATE and RDATE to - # the same type as DTDSTART + # the same type as DTSTART if hasattr(component, "dtstart"): ref_date = component.dtstart.value ref_value_param = component.dtstart.params.get("VALUE") for dates in chain(component.contents.get("exdate", []), component.contents.get("rdate", [])): + for i, date in enumerate(dates.value): + if type(ref_date) is datetime.datetime and type(date) is datetime.datetime: + if hasattr(ref_date, 'tzinfo') and ref_date.tzinfo is not None: + logger.trace("ITEM/check_and_sanitize_item: dtstart has tzinfo: '%s'", ref_date) + if hasattr(date, 'tzinfo') and date.tzinfo is None: + # Ensure that datetime.datetime object has timezone set if dtstart has + dates.value[i] = dates.value[i].replace(tzinfo=ref_date.tzinfo) + logger.trace("ITEM/check_and_sanitize_item: overtake missing tzinfo from dtstart: '%s' -> '%s'", date, dates.value[i]) + elif (hasattr(ref_date, 'tzinfo') and ref_date.tzinfo is None) or not hasattr(ref_date, 'tzinfo'): + logger.trace("ITEM/check_and_sanitize_item: dtstart has no tzinfo: '%s'", ref_date) + if hasattr(date, 'tzinfo') and date.tzinfo is not None: + # Ensure that datetime.datetime object has no timezone set if dtstart has none + dates.value[i] = dates.value[i].replace(tzinfo=None) + logger.trace("ITEM/check_and_sanitize_item: remove existing tzinfo (dtstart has none): '%s' -> '%s'", date, dates.value[i]) if all(type(d) is type(ref_date) for d in dates.value): continue for i, date in enumerate(dates.value): diff --git a/radicale/tests/static/event_issue2151.ics b/radicale/tests/static/event_issue2151.ics new file mode 100644 index 00000000..a72d3731 --- /dev/null +++ b/radicale/tests/static/event_issue2151.ics @@ -0,0 +1,116 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:W. Europe Standard Time +BEGIN:STANDARD +DTSTART:16010101T030000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20260605T085814Z +LAST-MODIFIED:20260605T100722Z +DTSTAMP:20260605T100722Z +UID:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +SUMMARY:Billings-task-setting follow-up series +PRIORITY:5 +STATUS:CONFIRMED +ORGANIZER;CN=XXXXXXXX XXXXX:mailto:XXXXXXX@XXXXXXXXXXXXXX.XXX +ATTENDEE;RSVP=TRUE;CN=XX;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:XX@XXXXXXX.XXX +ATTENDEE;RSVP=TRUE;CN=XXXXXXXX XXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXX@XXXXXXXXXXXXXX.XXX +ATTENDEE;RSVP=TRUE;CN=XXXXXX XXXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXXX@XXXXXXXXXXXXXX.XXX +EXDATE:20260403T110000 +RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20260731T090000Z +X-MOZ-LASTACK:20260605T100722Z +DTSTART;TZID=W. Europe Standard Time:20260123T110000 +DTEND;TZID=W. Europe Standard Time:20260123T120000 +CLASS:PUBLIC +DESCRIPTION;LANGUAGE=de-DE:Serie war letzte Woche ausgelaufen\n\nMicrosoft Teams meeting details redacted.\n +LOCATION;LANGUAGE=de-DE:Microsoft Teams-Besprechung +SEQUENCE:0 +TRANSP:OPAQUE +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-APPT-SEQUENCE:0 +X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:1 +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-OWNERAPPTID:XXXXXXXXXX +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-ISRESPONSEREQUESTED:TRUE +X-MICROSOFT-LOCATIONDISPLAYNAME:Microsoft Teams-Besprechung +X-MICROSOFT-LOCATIONS:[{"DisplayName":"Microsoft Teams-Besprechung"\,"LocationAnnotation":""\,"LocationUri":""\,"LocationStreet":""\,"LocationCity":""\,"LocationState":""\,"LocationCountry":""\,"LocationPostalCode":""\,"LocationFullAddress":""}] +X-MICROSOFT-LOCATIONSOURCE:None +X-MICROSOFT-ONLINEMEETINGINFORMATION:{"OnlineMeetingChannelId":null\,"OnlineMeetingProvider":3} +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-SCHEDULINGSERVICEUPDATEURL:REDACTED +X-MICROSOFT-SKYPETEAMSMEETINGURL:REDACTED +X-MICROSOFT-SKYPETEAMSPROPERTIES:REDACTED +X-MOZ-GENERATION:17 +X-MOZ-INVITED-ATTENDEE:mailto:XX@XXXXXXX.XXX +X-MOZ-RECEIVED-DTSTAMP:20260123T100333Z +X-MOZ-RECEIVED-SEQUENCE:0 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT5M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +BEGIN:VEVENT +CREATED:20260605T085814Z +LAST-MODIFIED:20260130T093740Z +DTSTAMP:20260130T093740Z +UID:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +SUMMARY:Billings-task-setting follow-up series +PRIORITY:5 +STATUS:CONFIRMED +RECURRENCE-ID;TZID=W. Europe Standard Time:20260130T110000 +ORGANIZER;CN=XXXXXXXX XXXXX:mailto:XXXXXXX@XXXXXXXXXXXXXX.XXX +ATTENDEE;RSVP=TRUE;CN=XXXXXXXX XXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXX@XXXXXXXXXXXXXX.XXX +ATTENDEE;RSVP=TRUE;CN=XXXXXX XXXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXXX@XXXXXXXXXXXXXX.XXX +ATTENDEE;RSVP=TRUE;CN=XX;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:XX@XXXXXXX.XXX +DTSTART;TZID=W. Europe Standard Time:20260130T103000 +DTEND;TZID=W. Europe Standard Time:20260130T113000 +CLASS:PUBLIC +DESCRIPTION:Serie war letzte Woche ausgelaufen\n\nMicrosoft Teams meeting details redacted.\n +LOCATION:Microsoft Teams-Besprechung +SEQUENCE:1 +TRANSP:OPAQUE +X-MICROSOFT-CDO-ALLDAYEVENT:FALSE +X-MICROSOFT-CDO-APPT-SEQUENCE:1 +X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE +X-MICROSOFT-CDO-IMPORTANCE:1 +X-MICROSOFT-CDO-INSTTYPE:3 +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MICROSOFT-CDO-OWNERAPPTID:XXXXXXXXXX +X-MICROSOFT-DISALLOW-COUNTER:FALSE +X-MICROSOFT-DONOTFORWARDMEETING:FALSE +X-MICROSOFT-ISRESPONSEREQUESTED:TRUE +X-MICROSOFT-LOCATIONDISPLAYNAME:Microsoft Teams-Besprechung +X-MICROSOFT-LOCATIONS:[{"DisplayName":"Microsoft Teams-Besprechung"\,"LocationAnnotation":""\,"LocationUri":""\,"LocationStreet":""\,"LocationCity":""\,"LocationState":""\,"LocationCountry":""\,"LocationPostalCode":""\,"LocationFullAddress":""}] +X-MICROSOFT-LOCATIONSOURCE:None +X-MICROSOFT-ONLINEMEETINGINFORMATION:{"OnlineMeetingChannelId":null\,"OnlineMeetingProvider":3} +X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT +X-MICROSOFT-SCHEDULINGSERVICEUPDATEURL:REDACTED +X-MICROSOFT-SKYPETEAMSMEETINGURL:REDACTED +X-MICROSOFT-SKYPETEAMSPROPERTIES:REDACTED +X-MOZ-GENERATION:0 +X-MOZ-RECEIVED-DTSTAMP:20260130T093205Z +X-MOZ-RECEIVED-SEQUENCE:1 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT5M +DESCRIPTION:Mozilla Standardbeschreibung +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/radicale/tests/static/event_mixed_datetime_and_date_exdate.ics b/radicale/tests/static/event_mixed_datetime_and_date_exdate.ics new file mode 100644 index 00000000..7263daa6 --- /dev/null +++ b/radicale/tests/static/event_mixed_datetime_and_date_exdate.ics @@ -0,0 +1,33 @@ +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Paris +X-LIC-LOCATION:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20130902T150157Z +LAST-MODIFIED:20130902T150158Z +DTSTAMP:20130902T150158Z +UID:event_mixed_datetime_and_date_exdate +SUMMARY:Event +DTSTART:20130901T180000 +DTEND:20130901T190000 +RRULE:FREQ=DAILY;COUNT=3 +EXDATE:20130902T193000Z +END:VEVENT +END:VCALENDAR diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 10f8b40d..d17dc70f 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -273,6 +273,18 @@ permissions: RrWw""") event = get_file_content("event_exdate_without_rrule.ics") self.put("/calendar.ics/event.ics", event) + def test_add_event_exdate_no_tz(self) -> None: + """Test event where EXDATE has no tzinfo.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_issue2151.ics") + self.put("/calendar.ics/event_issue2151.ics", event) + + def test_add_event_dtstart_no_tz_exdate_tz(self) -> None: + """Test event where DTSTART has no tzinfo but EXDATE.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_mixed_datetime_and_date_exdate.ics") + self.put("/calendar.ics/event_mixed_datetime_and_date_exdate.ics", event) + def test_add_todo(self) -> None: """Add a todo.""" self.mkcalendar("/calendar.ics/")