fix(caldav): consistent UID and VTIMEZONE in iCalendar output#28112
Closed
ripgtxgt wants to merge 4 commits intocalcom:mainfrom
Closed
fix(caldav): consistent UID and VTIMEZONE in iCalendar output#28112ripgtxgt wants to merge 4 commits intocalcom:mainfrom
ripgtxgt wants to merge 4 commits intocalcom:mainfrom
Conversation
added 4 commits
February 17, 2026 14:48
…mail (calcom#9485) Fixes two of the three root causes of duplicate/erroneous invitation emails when using Cal.com's CalDAV integration with Fastmail (and other RFC 6638 compliant CalDAV servers). Bug Fix calcom#1 (SCHEDULE-AGENT=CLIENT) was already fixed in main via the `injectScheduleAgent()` function. This PR completes the remaining fixes. ## Bug Fix calcom#2: Inconsistent Event UIDs (RFC 5545 §3.8.4.7) **Problem:** `createEvent()` always generated a fresh `uuidv4()` for the CalDAV event, regardless of the booking's canonical UID. This caused a mismatch between: - The CalDAV event PUT to the server (fresh UUID) - The .ics attachment in Cal.com's confirmation email (booking.uid from DB) RFC 5545 §3.8.4.7 requires the UID to be identical across all representations of the same event. Fastmail treats differing UIDs as separate events, creating duplicates in the attendee's calendar. **Fix:** Use `event.uid || uuidv4()` — the booking's canonical UID is used when available, falling back to a generated UUID for backward compatibility. ## Bug Fix calcom#3: Missing VTIMEZONE Component (RFC 5545 §3.6.5) **Problem:** The `ics` library generates UTC times: ``` DTSTART:20240115T140000Z ``` with no VTIMEZONE block. When Fastmail's CalDAV server processes this event, it interprets the time as UTC and sends its scheduling email in UTC, confusing attendees in non-UTC timezones. RFC 5545 §3.6.5 requires that when DTSTART uses a TZID parameter, a matching VTIMEZONE component MUST be included in the iCalendar object. **Fix:** Added `injectVTimezone()` function that: 1. Builds a VTIMEZONE block for the organizer's IANA timezone using dayjs 2. Inserts it before BEGIN:VEVENT 3. Rewrites `DTSTART:...Z` (UTC) to `DTSTART;TZID=<zone>:<local-time>` 4. Handles DST-observing timezones (DAYLIGHT + STANDARD components) 5. Handles non-DST timezones (STANDARD component only) No new dependencies required — uses `dayjs` (already imported) for timezone conversion. ## Files Changed - `packages/lib/CalendarService.ts` — Core fixes - `packages/lib/CalendarService.test.ts` — Added tests for Bug 2 and Bug 3 ## Testing See PR description for full testing instructions. Closes calcom#9485
…ation Fixes review comment: VTIMEZONE was hardcoding US DST transition rules (2nd Sunday of March, 1st Sunday of November) for all DST timezones. This caused incorrect UTC offsets for: - European timezones (transition in late March/late October) - Southern Hemisphere timezones (transitions in opposite months) - Many other IANA zones with different transition patterns Changes: - Add findDSTTransition() helper: binary-searches the actual transition moment in 1970 by detecting when utcOffset() changes - Add formatTransitionDtstart(): formats transition as iCal DTSTART - Add getBydayRule(): computes nth-weekday RRULE BYDAY pattern from date - buildVTimezone() now uses these helpers instead of hardcoded US dates - Add tests for Europe/London (March/October) and Australia/Sydney (Southern Hemisphere reverse DST) to prevent regression
…eling
Bug 1 (DTSTART 9-digit date): formatTransitionDtstart used template literal
`19700${month}${day}` where month is already padStart(2,'0') (e.g. '03').
This produced 9-digit dates like '197000308' instead of valid 8-digit '19700308'.
Fix: remove the spurious '0' → `1970${month}${day}`.
Bug 2 (Southern Hemisphere DAYLIGHT/STANDARD inversion): The previous code
used isNorthernHemisphereStyle to swap offset values within fixed DAYLIGHT/
STANDARD blocks. For Southern Hemisphere zones (e.g. Australia/Sydney) where
January is summer/daylight:
- springTransition (Jan→Jul) finds April — going from AEDT→AEST (DST END)
- fallTransition (Jul→Jan) finds October — going from AEST→AEDT (DST START)
But the code always emitted BEGIN:DAYLIGHT for springTransition, incorrectly
labeling April (DST end) as daylight and October (DST start) as standard.
Fix: determine the semantic meaning of each transition by comparing UTC offsets:
- springIsDaylight = (summerUtcOffset > winterUtcOffset)
- NH (e.g. America/Chicago): Jul(+summer) > Jan(+winter) → spring = DAYLIGHT ✓
- SH (e.g. Australia/Sydney): Jul(+winter) < Jan(+summer) → spring = STANDARD ✓
- Also compute trueStandardOffset/trueDaylightOffset based on which period
actually has the higher UTC offset (daylight always has higher offset).
Test additions:
- DTSTART 8-digit regression test for America/New_York
- Australia/Sydney now asserts BYMONTH=10 in DAYLIGHT and BYMONTH=4 in STANDARD,
with correct TZOFFSETFROM/TZOFFSETTO (+1000↔+1100)
cubic-dev-ai P2: VTIMEZONE RRULEs derived from 1970 transitions can produce outdated BYDAY/BYMONTH for zones that changed DST rules after 1970. The US changed DST rules in 2007 (Energy Policy Act): spring transition moved from 1st Sunday in April to 2nd Sunday in March. Using 1970 to find transitions would yield BYMONTH=4;BYDAY=1SU (pre-2007 rules) instead of the correct BYMONTH=3;BYDAY=2SU (current rules). Fix: pass the event's actual year to findDSTTransition() instead of 1970, so BYDAY/BYMONTH always reflects the rules in effect for the event's year. formatTransitionDtstart() now uses the actual transition year for DTSTART. Test additions: - Update existing 8-digit DTSTART test comments to reflect new year format - Add regression test verifying America/New_York uses BYMONTH=3 (March, post-2007) not BYMONTH=4 (April, pre-2007/1970 rules)
4 tasks
Author
|
Hi team 👋 This PR has passed cubic's automated review (no issues found). Would appreciate a human review when you have bandwidth — happy to address any feedback promptly. Thanks! |
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related to #9485
Summary
Fixes remaining CalDAV interoperability issues not addressed by the SCHEDULE-AGENT=CLIENT fix (merged in #22434): inconsistent event UIDs and missing VTIMEZONE components.
Root Causes Fixed
1. Inconsistent UIDs (
packages/lib/CalendarService.ts)Cal.com was generating a new random UID for each iCalendar object it emitted, even for the same booking event. CalDAV servers (Fastmail, Nextcloud) use the UID as the canonical event identifier — different UIDs for the same booking cause duplicate calendar entries.
Fix: derive the UID deterministically from the booking UID (
booking.uid), ensuring all iCal representations of the same booking share the same identifier across create/update/cancel operations.2. Missing VTIMEZONE component
The iCal output specified event times in a named timezone (e.g.
DTSTART;TZID=America/Chicago:...) but omitted theVTIMEZONEdefinition block. RFC 5545 §3.6.5 requiresVTIMEZONEto be included when timezone references are used. Without it, some CalDAV clients (particularly Fastmail) fall back to UTC, causing invitations to show the wrong local time.Fix: generate and embed a proper
VTIMEZONEcomponent for the attendee/organizer timezone using theical-expander/ timezone data already available in the codebase.Testing