Skip to content

fix: prevent double timezone conversion for CalDAV events from Zimbra (#27877)#28026

Open
kiro-dev28 wants to merge 1 commit intocalcom:mainfrom
kiro-dev28:fix/caldav-zimbra-double-tz-conversion-27877
Open

fix: prevent double timezone conversion for CalDAV events from Zimbra (#27877)#28026
kiro-dev28 wants to merge 1 commit intocalcom:mainfrom
kiro-dev28:fix/caldav-zimbra-double-tz-conversion-27877

Conversation

@kiro-dev28
Copy link

What does this PR do?

Fixes CalDAV events synced from Zimbra showing a negative duration (end time before start time, e.g. start 10:00 AM, end 9:00 AM for a 15-minute slot).

Root Cause

When a CalDAV server sends an event with DTSTART;TZID=Europe/Berlin:20260105T090000, ical.js parses this into a zone-aware ICAL.Time that already correctly represents 09:00 local → 08:00 UTC.

The subsequent event.startDate.convertToZone(zone) call was being made unconditionally. convertToZone on a non-floating time re-interprets the local wall-clock value as UTC first, then applies the zone offset a second time:

convertToZone on 09:00 (already Europe/Berlin):
  step 1: treat 09:00 as UTC
  step 2: apply +01:00 offset → 10:00 local ← wrong (+1h drift)

For a 15-minute event this produces start=10:00, end=09:00 — a -1h duration.

Fix

1. Guard convertToZone with isFloating() — only convert times that ical.js left in floating (zone-less) form. Already-zoned times (isFloating() === false) are correctly resolved by ical.js and must not be converted again.

2. Improve VTIMEZONE lookup for vendor-prefixed TZIDs — Zimbra uses DTSTART;TZID=/zimbra.com/standard/Europe/Berlin but the VTIMEZONE component has TZID:Europe/Berlin. The exact-match lookup failed silently, causing a mismatched VTIMEZONE to be selected. Now falls back to extracting the IANA suffix after the last /.

Tests

Two new test cases added to CalendarService.test.ts:

  • Standard VTIMEZONE: verifies start=08:00 UTC (09:00 Berlin−1h), end > start
  • Zimbra vendor-prefixed TZID: verifies fallback lookup resolves correctly, end > start

Related

Fixes #27877


…calcom#27877)

Root cause: ical.js resolves DTSTART;TZID=... into a zone-aware ICAL.Time.
Calling convertToZone() on an already-zoned time re-interprets the local
value as UTC first, then applies the offset a second time. For a 09:00
Europe/Berlin event (UTC+1) this produces:

  convertToZone step 1: treat 09:00 as UTC → subtract 1h offset → 08:00
  convertToZone step 2: apply +1h offset again → 10:00 ← wrong

The 15-min slot then shows start=10:00, end=09:00 (unchanged) = -1h duration.

Fix:
1. Guard convertToZone with isFloating() — only call it when ical.js could
   not resolve a timezone (floating time), not when it already produced a
   zone-aware time.  This preserves the existing behaviour for iCal feeds
   without VTIMEZONE (where the time IS floating and conversion is needed).

2. Improve VTIMEZONE lookup for vendor-prefixed TZIDs (Zimbra uses
   /zimbra.com/standard/Europe/Berlin; Mozilla uses
   /mozilla.org/20070129_1/Europe/Berlin). If the exact-match lookup fails,
   extract the last path segment and retry — this matches the corresponding
   VTIMEZONE whose TZID is the plain IANA name.

Tests added in CalendarService.test.ts:
- Non-recurring event with standard VTIMEZONE: verifies start=08:00 UTC
  (09:00 Berlin), end > start (positive duration).
- Zimbra vendor-prefixed TZID: verifies the fallback lookup resolves the
  VTIMEZONE correctly and end > start.

Fixes calcom#27877
@CLAassistant
Copy link

CLAassistant commented Feb 18, 2026

CLA assistant check
All committers have signed the CLA.

@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Feb 18, 2026
@github-actions github-actions bot added the 🐛 bug Something isn't working label Feb 18, 2026
Copy link
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.

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:515">
P2: TZID fallback only takes the last segment (e.g., “Berlin”), so it won’t match standard IANA TZIDs like “Europe/Berlin”. This makes the vendor-prefixed lookup fail and can still select the wrong VTIMEZONE when multiple exist.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

// "/zimbra.com/standard/Europe/Berlin" or "/mozilla.org/20070129_1/Europe/Berlin".
// Extract the IANA part after the last "/" and retry the lookup.
if (!vtimezone && tzid.includes("/")) {
const ianaCandidate = tzid.split("/").pop();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 18, 2026

Choose a reason for hiding this comment

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

P2: TZID fallback only takes the last segment (e.g., “Berlin”), so it won’t match standard IANA TZIDs like “Europe/Berlin”. This makes the vendor-prefixed lookup fail and can still select the wrong VTIMEZONE when multiple exist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/lib/CalendarService.ts, line 515:

<comment>TZID fallback only takes the last segment (e.g., “Berlin”), so it won’t match standard IANA TZIDs like “Europe/Berlin”. This makes the vendor-prefixed lookup fail and can still select the wrong VTIMEZONE when multiple exist.</comment>

<file context>
@@ -505,7 +505,18 @@ export default abstract class BaseCalendarService implements Calendar {
+          // "/zimbra.com/standard/Europe/Berlin" or "/mozilla.org/20070129_1/Europe/Berlin".
+          // Extract the IANA part after the last "/" and retry the lookup.
+          if (!vtimezone && tzid.includes("/")) {
+            const ianaCandidate = tzid.split("/").pop();
+            if (ianaCandidate) {
+              vtimezone = allVtimezones.find((vtz) => vtz.getFirstPropertyValue("tzid") === ianaCandidate);
</file context>
Fix with Cubic

@285729101
Copy link

@zomars fixes double timezone conversion for Zimbra CalDAV events — was causing events to show at the wrong time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working community Created by Linear-GitHub Sync size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Events synced via CalDAV from Zimbra have end time before start time

3 participants

Comments