fix(ics): resolve Windows TZIDs from Outlook published calendars#1913
Open
kapej42 wants to merge 1 commit into
Open
fix(ics): resolve Windows TZIDs from Outlook published calendars#1913kapej42 wants to merge 1 commit into
kapej42 wants to merge 1 commit into
Conversation
…lumalpass#781, callumalpass#1085) Outlook's "publish to web" calendar feeds reference events with Windows- style TZIDs (e.g. `W. Europe Standard Time`, `Romance Standard Time`) but only inline VTIMEZONE blocks for some of the TZIDs referenced — typically just the calendar owner's primary zone. Events from invitees in other zones reference TZIDs that have no accompanying VTIMEZONE, and ical.js silently demotes those to floating time. `toUnixTime()` then interprets the wall clock as UTC, surfacing to the user as a fixed offset equal to their local zone (e.g. +2h in Europe/Amsterdam in summer). The existing `registerCalendarVTimezones` helper already handles VTIMEZONE blocks that *are* in the feed. This change adds a fallback for the missing ones: when ical.js ends up with a floating zone but the source property had a TZID parameter, resolve that TZID against the CLDR `windowsZones` table (Windows -> IANA) and convert the wall time to UTC via `Intl.DateTimeFormat`, which is fully populated with IANA tzdata in any modern JS runtime (Electron/Obsidian included). No new dependencies. The CLDR map is ~3 KB; the Intl-based conversion is ~30 lines. Existing VTIMEZONE-based resolution remains the fast path. - Add src/utils/icsTimezoneFallback.ts: - WINDOWS_TZID_TO_IANA (CLDR windowsZones, ~130 entries) - resolveTzidToIANA: passes through IANA names, maps Windows names, strips "(GMT+01:00) ..." prefixes some clients prepend - wallTimeInZoneToUtcIso: Intl-based wall-to-UTC with DST-gap retry - Modify ICSSubscriptionService.parseICS to read raw TZID parameters from DTSTART/DTEND (incl. modified recurring instances) and pass them to icalTimeToISOString for fallback resolution. - Switch recurring-instance end-time derivation to use the already- resolved start/end ISO strings, so fallback resolution applies to every instance, not just the first. - Extend the ical.js .d.ts shim with Component.getFirstProperty and a typed Time.zone shape; extend the test mock with getFirstProperty for parity. - Add 11 unit tests covering the Windows->IANA mapping, IANA pass- through, Intl-based wall-to-UTC conversion across CEST/CET/EST/PDT/ UTC/Etc-GMT zones, and DST-boundary behavior. Refs callumalpass#781 callumalpass#1085
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.
Fixes the bugs where ICS subscriptions from Microsoft Outlook (
/owa/calendar/.../calendar.ics) display events at the wrong time — typically with a fixed offset equal to the user's local zone (e.g. +2h in CEST, +1h in CET).Refs #781, #1085.
Repro
Outlook's published-calendar feeds reference events with Windows-style TZIDs and only inline a
VTIMEZONEblock for some of those TZIDs — typically the calendar owner's primary zone. Events from invitees in other zones reference TZIDs with no accompanyingVTIMEZONE. Example from a real feed:The user, viewing in Europe/Amsterdam (CEST = UTC+2), sees this event at 11:30 instead of 09:30.
Root cause
registerCalendarVTimezonescorrectly registersRomance Standard Time(because the feed inlines its VTIMEZONE) butW. Europe Standard Timeis unknown to ical.js.ICAL.Event#startDate.zonebecomesfloating.icalTimeToISOStringthen callstoUnixTime()on a floating time, which interprets the wall clock as UTC. When that ISO string is rendered in the user's local zone, it surfaces as a fixed offset (= user's UTC offset).ical.js 2.2.1 has no IANA tzdata built in (
ICAL.TimezoneService.has('Europe/Berlin')isfalseuntil a VTIMEZONE is registered), so it can't recover from a missing VTIMEZONE on its own.Fix
Add a fallback that activates only when ical.js has demoted a time to floating:
TZIDparameter from the source property (DTSTART/DTEND/recurring instances).windowsZonestable to an IANA name (e.g.W. Europe Standard Time→Europe/Berlin), or pass through if it's already a valid IANA name.Intl.DateTimeFormat, which is fully populated with IANA tzdata in Electron/Obsidian.The existing VTIMEZONE-resolved path remains the fast path — fallback only runs when
zone.tzid === 'floating'.Files
src/utils/icsTimezoneFallback.ts(new) — CLDR map + Intl-based wall-to-UTC conversionsrc/services/ICSSubscriptionService.ts— wire the fallback throughicalTimeToISOString, including the recurring-instance and modified-instance pathssrc/types/ical.d.ts— addComponent.getFirstPropertyand a typedTime.zoneshapetests/__mocks__/ical.ts— extend the mock withgetFirstPropertyfor parity with real ical.jstests/unit/utils/icsTimezoneFallback.test.ts(new) — 11 unit tests covering the Windows→IANA mapping, IANA pass-through, Intl-based wall-to-UTC conversion across CEST/CET/EST/PDT/UTC/Etc-GMT zones, and DST-boundary behaviorTrade-offs
toUnixTime()path untouched.Testing
releaseNoteserrors are pre-existing for the build-time generated file).