fix: CalDAV UID consistency and VTIMEZONE generation for Fastmail (#9485)#28007
fix: CalDAV UID consistency and VTIMEZONE generation for Fastmail (#9485)#28007kiro-dev28 wants to merge 4 commits intocalcom:mainfrom
Conversation
…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
Graphite Automations"Send notification to Community team when bounty PR opened" took an action on this PR • (02/17/26)2 teammates were notified to this PR based on Keith Williams's automation. |
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/lib/CalendarService.ts">
<violation number="1" location="packages/lib/CalendarService.ts:221">
P2: VTIMEZONE generation hardcodes U.S. DST transition rules for all timezones with DST. Many IANA zones (e.g., Europe, Southern Hemisphere) have different transition dates, so the injected VTIMEZONE will be wrong and can cause incorrect offsets around DST changes.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
…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
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/lib/CalendarService.ts">
<violation number="1" location="packages/lib/CalendarService.ts:223">
P2: DTSTART is formatted with `19700${month}${day}`, yielding a 9-digit date (e.g., 197000308) instead of the required 8-digit YYYYMMDD format. This makes generated VTIMEZONE DTSTART values invalid.</violation>
<violation number="2" location="packages/lib/CalendarService.ts:295">
P2: Southern Hemisphere DST transitions are labeled with fixed DAYLIGHT/ STANDARD components based on Jan→Jul vs Jul→Nov ranges. For zones where January is daylight and July is standard, this inverts the RRULE month/day (DAYLIGHT emitted for the DST end transition and STANDARD for the DST start), causing DST changes to be applied on the wrong dates.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
…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)
Addressing cubic review commentsDTSTART 9-digit date (P2, commit 9609244): Fixed by removing the spurious '0' from the template literal. The old code used Southern Hemisphere DST inversion (P2, commit 9609244): Fixed by computing the semantic meaning of each transition based on UTC offset comparison, rather than using a fixed Northern-Hemisphere assumption. The
Also fixed Unit tests added for both fixes (DTSTART 8-digit regression test for America/New_York, and Australia/Sydney DAYLIGHT/STANDARD month assertions). @cubic-dev-ai please re-review. |
@kiro-dev28 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/lib/CalendarService.ts">
<violation number="1" location="packages/lib/CalendarService.ts:280">
P2: VTIMEZONE RRULEs are derived from 1970 transitions, so BYMONTH/BYDAY can reflect outdated DST rules for zones where transitions changed after 1970, leading to incorrect local times for recurring events.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
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)
|
All cubic-dev-ai review comments addressed in
@cubic-dev-ai please re-review. |
@kiro-dev28 I have started the AI code review. It will take a few minutes to complete. |
|
I have read the CLA Document and I hereby sign the CLA |
|
@kiro-dev28 What is the reason you made this PR when the issue is already closed and the fix has been merged? |
|
Hi @anikdhabal, thanks for the question — it's totally fair to ask. Issue #9485 was indeed closed on 2026-01-22 when the SCHEDULE-AGENT=CLIENT fix was merged. However, looking at the original issue report, there were three distinct root causes of the Fastmail duplicate-invitation problem:
The issue reporter explicitly called out all three problems (quote: "cal.com is sending the invitation of the event with a different UID" and "the event doesn't include a timezone and only specifies the time in UTC"). Merging the SCHEDULE-AGENT fix resolved enough of the pain to close the issue tracker entry, but bugs 2 and 3 remain in the codebase and still affect Fastmail users. This PR targets those remaining two bugs. I should have been clearer in the PR description — rather than saying "Closes #9485", I should have said something like "Partially addresses the remaining unresolved root causes from #9485". If the consensus is that closing the issue means these bugs won't be fixed, I understand and will close this PR. But if there's appetite to fully resolve the Fastmail integration, these two RFC compliance issues are real and reproducible. Happy to update the PR description to make the scope clearer. Thanks again for reviewing. |
|
Hi @anikdhabal, thanks for the question — it's totally fair to ask. You're right that issue #9485 was closed on 2026-01-22 when the SCHEDULE-AGENT=CLIENT fix was merged. However, when I reviewed the original issue report in detail, I noticed it documented three distinct root causes for the Fastmail duplicate-invitation problem:
The original reporter explicitly mentioned both of these remaining issues:
The SCHEDULE-AGENT fix resolved the most visible symptom (double emails), which was enough to close the tracker issue. But the UID and VTIMEZONE bugs remain in the codebase and continue to affect Fastmail users (and other RFC-compliant CalDAV servers like Nextcloud, Apple Calendar Server, etc.). My mistake: I should have titled this PR differently. Rather than saying "Closes #9485", I should have framed it as "Addresses remaining RFC compliance issues from #9485" or opened a new tracking issue. I apologize for the confusion. What I'd like to understand:
I'm happy to adjust the framing or close this PR if the team doesn't want to pursue these fixes at this time. Just want to make sure we're aligned on the value and approach. Thanks for your time reviewing! |
Fix: CalDAV UID Consistency and VTIMEZONE for Fastmail
Closes #9485 | Bounty: https://algora.io/cal/bounties/9485
Summary
This PR resolves two of the three root causes of duplicate and erroneous invitation emails when using Cal.com's CalDAV integration with Fastmail (and other RFC 6638-compliant CalDAV servers). The third fix (SCHEDULE-AGENT=CLIENT) was already merged previously via
injectScheduleAgent().After this fix:
SCHEDULE-AGENT=CLIENTBackground
When a Fastmail user connects their CalDAV calendar to Cal.com:
.icsattachment (via its email system)Because Fastmail supports RFC 6638 scheduling, it was:
Changes
File:
packages/lib/CalendarService.tsFix #2: UID Consistency (RFC 5545 §3.8.4.7)
Before:
After:
The
CalendarEventtype already hasuid?: string | null, and the booking's uid is passed through theCalendarServiceEvent. This ensures the CalDAV event UID matches the uid in the email .ics attachment, so calendar clients (Fastmail, Apple Calendar, etc.) treat them as the same event rather than creating duplicates.Fix #3: VTIMEZONE Component (RFC 5545 §3.6.5)
Added two new functions:
buildVTimezone(timezone, eventStart)— Generates a VTIMEZONE iCalendar block for any IANA timezone:injectVTimezone(iCalString, timezone, startTime, endTime)— Post-processes theicslibrary output to:DTSTART:20240115T140000Z(UTC) withDTSTART;TZID=America/Chicago:20240115T080000(local time)DTENDsimilarly (when present)BEGIN:VEVENTNo new dependencies — uses
dayjswhich is already imported.Before (what
icslibrary generates):After (with this fix):
Both fixes are also applied to
updateEvent().File:
packages/lib/CalendarService.test.tsAdded test suites for both fixes:
UID Consistency tests:
event.uidwhen providedevent.uidis undefined/nullVTIMEZONE tests:
TZID=parameter instead of UTC Z suffixupdateEvent()as wellTesting Instructions
Unit Tests
All existing SCHEDULE-AGENT tests should still pass. New UID and VTIMEZONE tests should pass.
Manual Testing with a Real CalDAV Server
The best way to verify is to test against a live Fastmail CalDAV account:
Verifying the ICS Output
You can inspect the generated ICS by temporarily adding a
console.log:Expected output for an organizer in
America/Chicago:Other CalDAV Providers
This change should be backward-compatible with other CalDAV providers:
The VTIMEZONE approach is the standard, recommended way to express event times in CalDAV per RFC 5545.
Related Issues
RFC References