Skip to content

[CalendarLink] Recurring events drift by an hour across DST (UTC-only serialization) #3811

Description

@ker0x

Component: CalendarLink
Version: v3.4.0

Problem

IcsBuilder converts every timed value to UTC and emits no VTIMEZONE
component and no TZID parameter:

private function formatUtc(\DateTimeInterface $dt): string
{
    return \DateTimeImmutable::createFromInterface($dt)
        ->setTimezone(new \DateTimeZone('UTC'))
        ->format('Ymd\THis\Z');
}

For a single event this is fine — an absolute instant is an absolute instant.
It stops being fine once CalendarRecurrence is used, which the component
ships and documents.

A weekly 09:00 Europe/Paris event created in July serializes to
DTSTART:...T070000Z. Under RRULE:FREQ=WEEKLY, occurrences after the October
DST transition remain 07:00 UTC — which is 08:00 Paris. The series silently
shifts by an hour for half the year.

RFC 5545 §3.8.5.3 is explicit that recurrence rules are expanded in the time
zone of DTSTART, which is why recurring events are normally anchored with
DTSTART;TZID=Europe/Paris:... plus a matching VTIMEZONE.

Reproduction

$event = new CalendarEvent(
    title: 'Weekly standup',
    start: new \DateTimeImmutable('2026-07-01 09:00', new \DateTimeZone('Europe/Paris')),
    end:   new \DateTimeImmutable('2026-07-01 09:30', new \DateTimeZone('Europe/Paris')),
    recurrence: CalendarRecurrence::weekly(until: new \DateTimeImmutable('2026-12-31')),
);

Occurrences before 2026-10-25 land at 09:00 local; occurrences after land at
08:00 local.

Suggested fix

Emit DTSTART;TZID=<zone> / DTEND;TZID=<zone> from the incoming
\DateTimeInterface's own zone and write the corresponding VTIMEZONE.

If shipping a full VTIMEZONE generator is judged out of scope, a narrower fix
would still help: document the constraint, and consider rejecting or warning on
a CalendarRecurrence whose DTSTART carries a zone with DST transitions,
rather than emitting a silently wrong series.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions