diff --git a/CHANGELOG.md b/CHANGELOG.md index b216972a..559a2d5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Extension: item verification on commandline * Improvement: catch lack of support of PERIOD in vobject <= 0.9.9 * Fix: sharing: backmap of REPORT/PROPPATCH hrefs is now URL-encode-aware (edit of a shared collection failed when the principal contains '@') +* Workaround: remove trailing spaces on TZID and TZNAME appended by buggy Microsoft clients ## 3.7.5 * Add: [sharing] conversion_bday_summary_template (customize summary) diff --git a/radicale/item/__init__.py b/radicale/item/__init__.py index 17cb66fb..7a33ce03 100644 --- a/radicale/item/__init__.py +++ b/radicale/item/__init__.py @@ -69,6 +69,9 @@ def read_components(s: str) -> List[vobject.base.Component]: s = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', s) # Workaround delete all empty lines to avoid vobject parsing errors s = re.sub(r'(?m)^[ \t]*\r?\n', '', s) + # Delete trailing space on TZID and TZNAME (Microsoft Outlook bug) + s = re.sub(r'(TZID|TZNAME)(:[^\r\n]+) (\r?\n)', r"\1\2\3", s) # fix VTIMEZONE + s = re.sub(r'(TZID=[^:]+) (:)', r"\1\2", s) # fix DTSTART/DTEND return list(vobject.readComponents(s, allowQP=True)) diff --git a/radicale/tests/static/event_issue2172.ics b/radicale/tests/static/event_issue2172.ics new file mode 100644 index 00000000..a4a9cd16 --- /dev/null +++ b/radicale/tests/static/event_issue2172.ics @@ -0,0 +1,44 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +BEGIN:VTIMEZONE +TZID:Mitteleuropäische Zeit +BEGIN:STANDARD +DTSTART:20001025T020000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 +TZNAME:Mitteleuropäische Zeit +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20000329T010000 +RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 +TZNAME:Mitteleuropäische Sommerzeit +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:20260619T141049Z_FED000F14A0 +DTSTART;TZID=Mitteleuropäische Zeit :20260701T170000 +DTEND;TZID=Mitteleuropäische Zeit :20260701T183000 +ATTENDEE;CN=Mario Voigt;CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:REDACTED@REDACTED +CLASS:PUBLIC +DESCRIPTION:REDACTED +DTSTAMP:20260621T173336Z +LAST-MODIFIED:20260621T173336Z +LOCATION:Stadtwerkstatt +ORGANIZER;CN=REDACTED;ROLE=CHAIR:mailto:REDACTED +PRIORITY:5 +SUMMARY:REDACTED +TRANSP:OPAQUE +X-GWBOX-TYPE:SENT +X-GWCLASS:NORMAL +X-GWITEM-TYPE:APPOINTMENT +X-GWSHOW-AS:BUSY +X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY +X-MOZ-INVITED-ATTENDEE:mailto:REDACTED@REDACTED +X-MOZ-RECEIVED-DTSTAMP:20260621T173336Z +X-MOZ-RECEIVED-SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 38775595..1c934f6a 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -193,6 +193,25 @@ permissions: RrWw""") assert "RDATE;VALUE=PERIOD:20000102T000000Z/20000402T000000Z,20010102T000000Z/20010" in answer assert " 402T000000Z" in answer + def test_add_event_with_tz_trailing_space(self) -> None: + """Add an event.""" + self.mkcalendar("/calendar.ics/") + event = get_file_content("event_issue2172.ics") + path = "/calendar.ics/event_issue2172.ics" + self.put(path, event) + _, headers, answer = self.request("GET", path, check=200) + assert "ETag" in headers + assert headers["Content-Type"] == "text/calendar; charset=utf-8" + assert "DESCRIPTION" in answer + assert "TZID:Mitteleuropäische Zeit " not in answer + assert "TZID:Mitteleuropäische Zeit" in answer + assert "TZNAME:Mitteleuropäische Zeit " not in answer + assert "TZNAME:Mitteleuropäische Zeit" in answer + assert "DTSTART;TZID=Mitteleuropäische Zeit :20260701T170000" not in answer + assert "DTSTART;TZID=Mitteleuropäische Zeit:20260701T170000" in answer + assert "DTEND;TZID=Mitteleuropäische Zeit :20260701T183000" not in answer + assert "DTEND;TZID=Mitteleuropäische Zeit:20260701T183000" in answer + def test_add_event_with_desc_ok(self) -> None: """Add an event.""" self.mkcalendar("/calendar.ics/")