Fix 500 on REPORT for recurring events with Windows timezone IDs

DAVx5 reported a 500 Internal Server Error on a time-range REPORT
against a calendar containing a recurring event whose DTSTART/DTEND used
a Windows-style TZID (e.g. "W. Europe Standard Time", as commonly written
by Outlook/Exchange and some Thunderbird/Lightning setups) instead of an
IANA zone name.

go-ical resolves TZID via a plain time.LoadLocation call, which only
understands IANA names. A simple decode of such an event succeeds (RRULE
dates aren't parsed eagerly), but expanding its recurrence - which
go-webdav's caldav.Filter does for every time-range REPORT - calls
Component.RecurrenceSet, which does call time.LoadLocation(tzid) and
fails with "ical: error parsing start time: unknown time zone ...".

Add internal/icalfix, a small shared helper that rewrites recognized
Windows timezone identifiers (TZID parameters and VTIMEZONE TZID: lines)
to their IANA equivalent in raw ICS bytes before decoding. Wire it into
every ical.NewDecoder call site: internal/caldav/backend.go's
decodeObject (fixes the reported bug), internal/web/calendar.go's event
rendering/ICS import, and internal/icssub's remote feed fetching, so a
subscribed feed with the same issue doesn't hit it either.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-20 22:23:11 +02:00
co-authored by Copilot
parent 1707ab4060
commit 213b22f821
5 changed files with 295 additions and 5 deletions
+4 -3
View File
@@ -18,6 +18,7 @@ import (
ical "github.com/emersion/go-ical"
"github.com/yourusername/caldav-server/internal/birthdays"
"github.com/yourusername/caldav-server/internal/db"
"github.com/yourusername/caldav-server/internal/icalfix"
"github.com/yourusername/caldav-server/internal/web/templates"
)
@@ -675,7 +676,7 @@ func (s *Server) saveEventFromForm(w http.ResponseWriter, r *http.Request, owner
http.NotFound(w, r)
return
}
existing, err := ical.NewDecoder(strings.NewReader(string(data))).Decode()
existing, err := ical.NewDecoder(bytes.NewReader(icalfix.NormalizeTimeZones(data))).Decode()
if err != nil {
s.logger.Error("decoding existing event", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
@@ -791,7 +792,7 @@ func (s *Server) saveEvent(owner, cal, id string, calendar *ical.Calendar) error
// EventFormData shape used both for pre-filling the edit form and for
// placing the event on the month grid.
func eventFormFromICS(id string, data []byte) (templates.EventFormData, error) {
calendar, err := ical.NewDecoder(strings.NewReader(string(data))).Decode()
calendar, err := ical.NewDecoder(bytes.NewReader(icalfix.NormalizeTimeZones(data))).Decode()
if err != nil {
return templates.EventFormData{}, err
}
@@ -974,7 +975,7 @@ func (s *Server) handleCalendarImport(w http.ResponseWriter, r *http.Request) {
}
imported := 0
dec := ical.NewDecoder(bytes.NewReader(body))
dec := ical.NewDecoder(bytes.NewReader(icalfix.NormalizeTimeZones(body)))
for {
srcCal, err := dec.Decode()
if errors.Is(err, io.EOF) {