Skip to content

Commit 77bef83

Browse files
authored
Merge pull request #2153 from pbiering/issue-2151
Improve: sanitize item add timezone to EXDATE or RDATE if missing but DTSTART has
2 parents e4f70c3 + e933725 commit 77bef83

5 files changed

Lines changed: 177 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* Add: [sharing] conversion_bday_categories (customize)
88
* Add: [sharing] conversion_bday_age_max (limit in case of "age" placeholder is used which blocks using RRULE)
99
* Extension: [sharing/bday conversion]: add STATUS + CLASS fields
10+
* Improve: sanitize item align timezone (add/remove) of EXDATE or RDATE with DTSTART
1011

1112
## 3.7.4
1213
* Fix: sharing: PROPFIND returns now empty owner element in case of a mapped share as clients try PROPFIND on this not accessable href

radicale/item/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,26 @@ def check_and_sanitize_items(
174174
# EXDATE has value DATE even if DTSTART/DTEND is DATE-TIME.
175175
# The RFC is vaguely formulated on the issue.
176176
# To resolve the issue convert EXDATE and RDATE to
177-
# the same type as DTDSTART
177+
# the same type as DTSTART
178178
if hasattr(component, "dtstart"):
179179
ref_date = component.dtstart.value
180180
ref_value_param = component.dtstart.params.get("VALUE")
181181
for dates in chain(component.contents.get("exdate", []),
182182
component.contents.get("rdate", [])):
183+
for i, date in enumerate(dates.value):
184+
if type(ref_date) is datetime.datetime and type(date) is datetime.datetime:
185+
if hasattr(ref_date, 'tzinfo') and ref_date.tzinfo is not None:
186+
logger.trace("ITEM/check_and_sanitize_item: dtstart has tzinfo: '%s'", ref_date)
187+
if hasattr(date, 'tzinfo') and date.tzinfo is None:
188+
# Ensure that datetime.datetime object has timezone set if dtstart has
189+
dates.value[i] = dates.value[i].replace(tzinfo=ref_date.tzinfo)
190+
logger.trace("ITEM/check_and_sanitize_item: overtake missing tzinfo from dtstart: '%s' -> '%s'", date, dates.value[i])
191+
elif (hasattr(ref_date, 'tzinfo') and ref_date.tzinfo is None) or not hasattr(ref_date, 'tzinfo'):
192+
logger.trace("ITEM/check_and_sanitize_item: dtstart has no tzinfo: '%s'", ref_date)
193+
if hasattr(date, 'tzinfo') and date.tzinfo is not None:
194+
# Ensure that datetime.datetime object has no timezone set if dtstart has none
195+
dates.value[i] = dates.value[i].replace(tzinfo=None)
196+
logger.trace("ITEM/check_and_sanitize_item: remove existing tzinfo (dtstart has none): '%s' -> '%s'", date, dates.value[i])
183197
if all(type(d) is type(ref_date) for d in dates.value):
184198
continue
185199
for i, date in enumerate(dates.value):
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
3+
VERSION:2.0
4+
BEGIN:VTIMEZONE
5+
TZID:W. Europe Standard Time
6+
BEGIN:STANDARD
7+
DTSTART:16010101T030000
8+
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
9+
TZOFFSETFROM:+0200
10+
TZOFFSETTO:+0100
11+
END:STANDARD
12+
BEGIN:DAYLIGHT
13+
DTSTART:16010101T020000
14+
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
15+
TZOFFSETFROM:+0100
16+
TZOFFSETTO:+0200
17+
END:DAYLIGHT
18+
END:VTIMEZONE
19+
BEGIN:VEVENT
20+
CREATED:20260605T085814Z
21+
LAST-MODIFIED:20260605T100722Z
22+
DTSTAMP:20260605T100722Z
23+
UID:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
24+
SUMMARY:Billings-task-setting follow-up series
25+
PRIORITY:5
26+
STATUS:CONFIRMED
27+
ORGANIZER;CN=XXXXXXXX XXXXX:mailto:XXXXXXX@XXXXXXXXXXXXXX.XXX
28+
ATTENDEE;RSVP=TRUE;CN=XX;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:XX@XXXXXXX.XXX
29+
ATTENDEE;RSVP=TRUE;CN=XXXXXXXX XXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXX@XXXXXXXXXXXXXX.XXX
30+
ATTENDEE;RSVP=TRUE;CN=XXXXXX XXXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXXX@XXXXXXXXXXXXXX.XXX
31+
EXDATE:20260403T110000
32+
RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20260731T090000Z
33+
X-MOZ-LASTACK:20260605T100722Z
34+
DTSTART;TZID=W. Europe Standard Time:20260123T110000
35+
DTEND;TZID=W. Europe Standard Time:20260123T120000
36+
CLASS:PUBLIC
37+
DESCRIPTION;LANGUAGE=de-DE:Serie war letzte Woche ausgelaufen\n\nMicrosoft Teams meeting details redacted.\n
38+
LOCATION;LANGUAGE=de-DE:Microsoft Teams-Besprechung
39+
SEQUENCE:0
40+
TRANSP:OPAQUE
41+
X-MICROSOFT-CDO-ALLDAYEVENT:FALSE
42+
X-MICROSOFT-CDO-APPT-SEQUENCE:0
43+
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
44+
X-MICROSOFT-CDO-IMPORTANCE:1
45+
X-MICROSOFT-CDO-INSTTYPE:1
46+
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
47+
X-MICROSOFT-CDO-OWNERAPPTID:XXXXXXXXXX
48+
X-MICROSOFT-DISALLOW-COUNTER:FALSE
49+
X-MICROSOFT-DONOTFORWARDMEETING:FALSE
50+
X-MICROSOFT-ISRESPONSEREQUESTED:TRUE
51+
X-MICROSOFT-LOCATIONDISPLAYNAME:Microsoft Teams-Besprechung
52+
X-MICROSOFT-LOCATIONS:[{"DisplayName":"Microsoft Teams-Besprechung"\,"LocationAnnotation":""\,"LocationUri":""\,"LocationStreet":""\,"LocationCity":""\,"LocationState":""\,"LocationCountry":""\,"LocationPostalCode":""\,"LocationFullAddress":""}]
53+
X-MICROSOFT-LOCATIONSOURCE:None
54+
X-MICROSOFT-ONLINEMEETINGINFORMATION:{"OnlineMeetingChannelId":null\,"OnlineMeetingProvider":3}
55+
X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT
56+
X-MICROSOFT-SCHEDULINGSERVICEUPDATEURL:REDACTED
57+
X-MICROSOFT-SKYPETEAMSMEETINGURL:REDACTED
58+
X-MICROSOFT-SKYPETEAMSPROPERTIES:REDACTED
59+
X-MOZ-GENERATION:17
60+
X-MOZ-INVITED-ATTENDEE:mailto:XX@XXXXXXX.XXX
61+
X-MOZ-RECEIVED-DTSTAMP:20260123T100333Z
62+
X-MOZ-RECEIVED-SEQUENCE:0
63+
BEGIN:VALARM
64+
ACTION:DISPLAY
65+
TRIGGER:-PT5M
66+
DESCRIPTION:Mozilla Standardbeschreibung
67+
END:VALARM
68+
END:VEVENT
69+
BEGIN:VEVENT
70+
CREATED:20260605T085814Z
71+
LAST-MODIFIED:20260130T093740Z
72+
DTSTAMP:20260130T093740Z
73+
UID:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
74+
SUMMARY:Billings-task-setting follow-up series
75+
PRIORITY:5
76+
STATUS:CONFIRMED
77+
RECURRENCE-ID;TZID=W. Europe Standard Time:20260130T110000
78+
ORGANIZER;CN=XXXXXXXX XXXXX:mailto:XXXXXXX@XXXXXXXXXXXXXX.XXX
79+
ATTENDEE;RSVP=TRUE;CN=XXXXXXXX XXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXX@XXXXXXXXXXXXXX.XXX
80+
ATTENDEE;RSVP=TRUE;CN=XXXXXX XXXXXXXX;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:XXXXXXXXXX@XXXXXXXXXXXXXX.XXX
81+
ATTENDEE;RSVP=TRUE;CN=XX;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:XX@XXXXXXX.XXX
82+
DTSTART;TZID=W. Europe Standard Time:20260130T103000
83+
DTEND;TZID=W. Europe Standard Time:20260130T113000
84+
CLASS:PUBLIC
85+
DESCRIPTION:Serie war letzte Woche ausgelaufen\n\nMicrosoft Teams meeting details redacted.\n
86+
LOCATION:Microsoft Teams-Besprechung
87+
SEQUENCE:1
88+
TRANSP:OPAQUE
89+
X-MICROSOFT-CDO-ALLDAYEVENT:FALSE
90+
X-MICROSOFT-CDO-APPT-SEQUENCE:1
91+
X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
92+
X-MICROSOFT-CDO-IMPORTANCE:1
93+
X-MICROSOFT-CDO-INSTTYPE:3
94+
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
95+
X-MICROSOFT-CDO-OWNERAPPTID:XXXXXXXXXX
96+
X-MICROSOFT-DISALLOW-COUNTER:FALSE
97+
X-MICROSOFT-DONOTFORWARDMEETING:FALSE
98+
X-MICROSOFT-ISRESPONSEREQUESTED:TRUE
99+
X-MICROSOFT-LOCATIONDISPLAYNAME:Microsoft Teams-Besprechung
100+
X-MICROSOFT-LOCATIONS:[{"DisplayName":"Microsoft Teams-Besprechung"\,"LocationAnnotation":""\,"LocationUri":""\,"LocationStreet":""\,"LocationCity":""\,"LocationState":""\,"LocationCountry":""\,"LocationPostalCode":""\,"LocationFullAddress":""}]
101+
X-MICROSOFT-LOCATIONSOURCE:None
102+
X-MICROSOFT-ONLINEMEETINGINFORMATION:{"OnlineMeetingChannelId":null\,"OnlineMeetingProvider":3}
103+
X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT
104+
X-MICROSOFT-SCHEDULINGSERVICEUPDATEURL:REDACTED
105+
X-MICROSOFT-SKYPETEAMSMEETINGURL:REDACTED
106+
X-MICROSOFT-SKYPETEAMSPROPERTIES:REDACTED
107+
X-MOZ-GENERATION:0
108+
X-MOZ-RECEIVED-DTSTAMP:20260130T093205Z
109+
X-MOZ-RECEIVED-SEQUENCE:1
110+
BEGIN:VALARM
111+
ACTION:DISPLAY
112+
TRIGGER:-PT5M
113+
DESCRIPTION:Mozilla Standardbeschreibung
114+
END:VALARM
115+
END:VEVENT
116+
END:VCALENDAR
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
BEGIN:VCALENDAR
2+
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
3+
VERSION:2.0
4+
BEGIN:VTIMEZONE
5+
TZID:Europe/Paris
6+
X-LIC-LOCATION:Europe/Paris
7+
BEGIN:DAYLIGHT
8+
TZOFFSETFROM:+0100
9+
TZOFFSETTO:+0200
10+
TZNAME:CEST
11+
DTSTART:19700329T020000
12+
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
13+
END:DAYLIGHT
14+
BEGIN:STANDARD
15+
TZOFFSETFROM:+0200
16+
TZOFFSETTO:+0100
17+
TZNAME:CET
18+
DTSTART:19701025T030000
19+
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
20+
END:STANDARD
21+
END:VTIMEZONE
22+
BEGIN:VEVENT
23+
CREATED:20130902T150157Z
24+
LAST-MODIFIED:20130902T150158Z
25+
DTSTAMP:20130902T150158Z
26+
UID:event_mixed_datetime_and_date_exdate
27+
SUMMARY:Event
28+
DTSTART:20130901T180000
29+
DTEND:20130901T190000
30+
RRULE:FREQ=DAILY;COUNT=3
31+
EXDATE:20130902T193000Z
32+
END:VEVENT
33+
END:VCALENDAR

radicale/tests/test_base.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,18 @@ def test_add_event_with_exdate_without_rrule(self) -> None:
273273
event = get_file_content("event_exdate_without_rrule.ics")
274274
self.put("/calendar.ics/event.ics", event)
275275

276+
def test_add_event_exdate_no_tz(self) -> None:
277+
"""Test event where EXDATE has no tzinfo."""
278+
self.mkcalendar("/calendar.ics/")
279+
event = get_file_content("event_issue2151.ics")
280+
self.put("/calendar.ics/event_issue2151.ics", event)
281+
282+
def test_add_event_dtstart_no_tz_exdate_tz(self) -> None:
283+
"""Test event where DTSTART has no tzinfo but EXDATE."""
284+
self.mkcalendar("/calendar.ics/")
285+
event = get_file_content("event_mixed_datetime_and_date_exdate.ics")
286+
self.put("/calendar.ics/event_mixed_datetime_and_date_exdate.ics", event)
287+
276288
def test_add_todo(self) -> None:
277289
"""Add a todo."""
278290
self.mkcalendar("/calendar.ics/")

0 commit comments

Comments
 (0)