Skip to content

fix(caldav): consistent UID and VTIMEZONE in iCalendar output#28112

Closed
ripgtxgt wants to merge 4 commits intocalcom:mainfrom
ripgtxgt:fix/caldav-uid-vtimezone-9485
Closed

fix(caldav): consistent UID and VTIMEZONE in iCalendar output#28112
ripgtxgt wants to merge 4 commits intocalcom:mainfrom
ripgtxgt:fix/caldav-uid-vtimezone-9485

Conversation

@ripgtxgt
Copy link
Copy Markdown

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 the VTIMEZONE definition block. RFC 5545 §3.6.5 requires VTIMEZONE to 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 VTIMEZONE component for the attendee/organizer timezone using the ical-expander / timezone data already available in the codebase.

Testing

  1. Connect a Fastmail or Nextcloud CalDAV calendar to Cal.com
  2. Book a meeting — verify single calendar entry appears (no duplicate)
  3. Reschedule — verify existing entry updates (same UID) rather than creating a new entry
  4. Verify invitation email shows correct local time (not UTC)

kiro-dev28 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)
@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Feb 21, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

@ripgtxgt
Copy link
Copy Markdown
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!

@anikdhabal
Copy link
Copy Markdown
Contributor

thanks for your work @ripgtxgt
Going with this one:- #28115

@anikdhabal anikdhabal closed this Feb 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Created by Linear-GitHub Sync size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants