From a20791e0c3f74098fae597de9bd3a8f3256d279c Mon Sep 17 00:00:00 2001 From: Unrud Date: Mon, 20 Dec 2021 00:55:29 +0100 Subject: [PATCH] Convert EXDATE and RDATE to same type as DTSTART Fixes #1146 Closes #1199 --- radicale/item/__init__.py | 28 ++++++++++++++-- .../static/event_mixed_datetime_and_date.ics | 33 +++++++++++++++++++ radicale/tests/test_base.py | 6 ++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 radicale/tests/static/event_mixed_datetime_and_date.ics diff --git a/radicale/item/__init__.py b/radicale/item/__init__.py index e715e2c..5d52c7c 100644 --- a/radicale/item/__init__.py +++ b/radicale/item/__init__.py @@ -24,11 +24,13 @@ Module for address books and calendar entries (see ``Item``). """ import binascii +import contextlib import math import os import sys from datetime import datetime, timedelta from hashlib import sha256 +from itertools import chain from typing import (Any, Callable, List, MutableMapping, Optional, Sequence, Tuple) @@ -142,6 +144,28 @@ def check_and_sanitize_items( logger.debug("Quirks: Removing zero duration from %s in " "object %r", component_name, component_uid) del component.duration + # Workaround for Evolution + # 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 + 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", [])): + replace_value_param = False + for i, date in enumerate(dates.value): + if type(date) != type(ref_date): + replace_value_param = True + dates.value[i] = ref_date.replace( + date.year, date.month, date.day) + if replace_value_param: + if ref_value_param is None: + with contextlib.suppress(KeyError): + del dates.params["VALUE"] + else: + dates.params["VALUE"] = ref_value_param # vobject interprets recurrence rules on demand try: component.rruleset @@ -176,9 +200,9 @@ def check_and_sanitize_items( else: vobject_item.add("UID").value = object_uid else: - for i in vobject_items: + for item in vobject_items: raise ValueError("Item type %r not supported in %s collection" % - (i.name, repr(tag) if tag else "generic")) + (item.name, repr(tag) if tag else "generic")) def check_and_sanitize_props(props: MutableMapping[Any, Any] diff --git a/radicale/tests/static/event_mixed_datetime_and_date.ics b/radicale/tests/static/event_mixed_datetime_and_date.ics new file mode 100644 index 0000000..241ef6f --- /dev/null +++ b/radicale/tests/static/event_mixed_datetime_and_date.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 +SUMMARY:Event +DTSTART;TZID=Europe/Paris:20130901T180000 +DTEND;TZID=Europe/Paris:20130901T190000 +RRULE:FREQ=DAILY;COUNT=3 +EXDATE;VALUE=DATE:20130902 +END:VEVENT +END:VCALENDAR diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index d0557e5..65f2556 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -97,6 +97,12 @@ permissions: RrWw""") assert xml.tag == xmlutils.make_clark("D:error") assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None + def test_add_event_with_mixed_datetime_and_date(self) -> None: + """Test event with DTSTART as DATE-TIME and EXDATE as DATE.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_mixed_datetime_and_date.ics") + self.put("/calendar.ics/event.ics", event) + def test_add_todo(self) -> None: """Add a todo.""" self.mkcalendar("/calendar.ics/")