From ff9ed32187bda8760f26d127589c93ead5a59cd3 Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Thu, 28 May 2026 22:10:19 +0200 Subject: [PATCH 1/3] fix parsing of DATE fields with TZID according to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.19 this is not valid; DATE fields must not have a TZID applied. *technically* i suppose this could be interpreted to mean one should just ignore the timezone parameter. though i'm convinced that is the intended meaning. in any case, ICal should support parsing such files. --- lib/ical/deserialize.ex | 15 +++++++------- test/data/date_only_event.ics | 36 ++++++++++++++++++++++++++++++++++ test/ical/deserialize_test.exs | 14 +++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 test/data/date_only_event.ics diff --git a/lib/ical/deserialize.ex b/lib/ical/deserialize.ex index 8909010..a5bb995 100644 --- a/lib/ical/deserialize.ex +++ b/lib/ical/deserialize.ex @@ -442,13 +442,6 @@ defmodule ICal.Deserialize do @spec to_date(String.t() | nil, map, ICal.t()) :: Date.t() | DateTime.t() | nil def to_date(nil, _params, _calendar), do: nil - def to_date(date_string, %{"TZID" => timezone}, %ICal{default_timezone: default_timezone}) do - # Microsoft Outlook calendar .ICS files report times in Greenwich Standard Time (UTC +0) - # so just convert this to UTC - timezone = to_timezone(timezone, default_timezone) - to_date_in_timezone(date_string, timezone) - end - def to_date(date_string, %{"VALUE" => "DATE"}, _calendar) do # of the form {YYYY}{MM}{DD} with <> <- date_string, @@ -462,6 +455,14 @@ defmodule ICal.Deserialize do end end + def to_date(date_string, %{"TZID" => timezone}, %ICal{default_timezone: default_timezone}) do + # Microsoft Outlook calendar .ICS files report times in Greenwich Standard Time (UTC +0) + # so just convert this to UTC + timezone = to_timezone(timezone, default_timezone) + + to_date_in_timezone(date_string, timezone) + end + def to_date(date_string, _params, %ICal{default_timezone: default_timezone}) do to_date_in_timezone(date_string, default_timezone) end diff --git a/test/data/date_only_event.ics b/test/data/date_only_event.ics new file mode 100644 index 0000000..75ce5d0 --- /dev/null +++ b/test/data/date_only_event.ics @@ -0,0 +1,36 @@ +BEGIN:VCALENDAR +METHOD:PUBLISH +PRODID:ExampleProd +VERSION:2.0 +X-WR-CALNAME:ExampleName +X-WR-TIMEZONE:America/Edmonton +BEGIN:VTIMEZONE +TZID:America/Edmonton +X-LIC-LOCATION:America/Edmonton +BEGIN:STANDARD +DTSTART:20241103T020000 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +TZNAME:MST +TZOFFSETFROM:-0600 +TZOFFSETTO:-0700 +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20250309T020000 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +TZNAME:MDT +TZOFFSETFROM:-0700 +TZOFFSETTO:-0600 +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +CREATED;TZID=America/Edmonton:20260406T000000 +DTEND;TZID=America/Edmonton;VALUE=DATE:20260530 +DTSTAMP;TZID=America/Edmonton:20260406T000000 +DTSTART;TZID=America/Edmonton;VALUE=DATE:20260529 +SEQUENCE:2026052815 +SUMMARY:ExampleSummary +UID:ExampleID +X-FUNAMBOL-ALLDAY:1 +X-MICROSOFT-CDO-BUSYSTATUS:BUSY +END:VEVENT +END:VCALENDAR diff --git a/test/ical/deserialize_test.exs b/test/ical/deserialize_test.exs index d53ed09..95da483 100644 --- a/test/ical/deserialize_test.exs +++ b/test/ical/deserialize_test.exs @@ -234,6 +234,20 @@ defmodule ICal.DeserializeTest do assert ICal.from_file("/does/not/exist.ics") == {:error, :enoent} end + test "Deserializing DATE fields with TZID params works correctly " do + # According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.19 + # this is not a RFC-conformant calendar, but ICal should still be able to parse it. + ics = Helper.test_data("date_only_event") + calendar = ICal.from_ics(ics) + + assert Enum.count(calendar.events) == 1 + + [event] = calendar.events + + assert event.dtstart != nil + assert event.dtend != nil + end + test "Bad separators do not disturb parsing" do ics = Helper.test_data("broken_uid") %ICal{events: [event]} = ICal.from_ics(ics) From 1846a15dce293f97cefc9ec99035bd3f178cb910 Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Thu, 28 May 2026 22:13:04 +0200 Subject: [PATCH 2/3] ICalendar -> ICal --- test/ical_test.exs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/ical_test.exs b/test/ical_test.exs index df5448b..8c0bc67 100644 --- a/test/ical_test.exs +++ b/test/ical_test.exs @@ -197,7 +197,7 @@ defmodule ICalTest do |> assert_fully_contains(expected) end - test "ICalender.to_ics/1 with exdates" do + test "ICal.to_ics/1 with exdates" do events = [ %ICal.Event{ exdates: [ @@ -216,7 +216,7 @@ defmodule ICalTest do assert ics =~ "EXDATE;TZID=America/Toronto:20200917T143000" end - test "ICalender.to_ics/1 with duration" do + test "ICal.to_ics/1 with duration" do events = [ %ICal.Event{ duration: %ICal.Duration{ @@ -236,7 +236,7 @@ defmodule ICalTest do assert ics =~ "DURATION:P15DT5H20S" end - test "ICalender.to_ics/1 with RECURRENCE-ID in UTC" do + test "ICal.to_ics/1 with RECURRENCE-ID in UTC" do events = [ %ICal.Event{ recurrence_id: ~U[2020-09-17 14:30:00Z], @@ -252,7 +252,7 @@ defmodule ICalTest do assert ics =~ "RECURRENCE-ID:20200917T143000Z" end - test "ICalender.to_ics/1 with RECURRENCE-ID with timezone" do + test "ICal.to_ics/1 with RECURRENCE-ID with timezone" do recurrence_id = DateTime.shift_zone!(~U[2020-09-17 18:30:00Z], "America/Toronto") events = [ @@ -270,7 +270,7 @@ defmodule ICalTest do assert ics =~ "RECURRENCE-ID;TZID=America/Toronto:20200917T143000" end - test "ICalender.to_ics/1 -> ICal.from_ics/1 and back again" do + test "ICal.to_ics/1 -> ICal.from_ics/1 and back again" do events = [ %ICal.Event{ summary: "Film with Amy and Adam", @@ -292,7 +292,7 @@ defmodule ICalTest do assert events |> List.first() == new_event end - test "ICalender.to_ics/1 -> ICal.from_ics/1 and back again, with newlines" do + test "ICal.to_ics/1 -> ICal.from_ics/1 and back again, with newlines" do events = [ %ICal.Event{ summary: "Film with Amy and Adam", @@ -318,7 +318,7 @@ defmodule ICalTest do Regex.scan(~r/#{check_for}/, ics) |> Enum.count() end - test "ICalender.to_ics/1 supports bare components and lists of components" do + test "ICal.to_ics/1 supports bare components and lists of components" do assert %ICal{events: [%ICal.Event{}]} |> ICal.to_ics() |> to_string() From fdd983c8f501ae64dbbb4bd28f2d787e0d731fb8 Mon Sep 17 00:00:00 2001 From: Aaron Seigo Date: Fri, 29 May 2026 00:18:32 +0200 Subject: [PATCH 3/3] use a positive assertion in the test --- test/ical/deserialize_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ical/deserialize_test.exs b/test/ical/deserialize_test.exs index 95da483..5b6c9ca 100644 --- a/test/ical/deserialize_test.exs +++ b/test/ical/deserialize_test.exs @@ -244,8 +244,8 @@ defmodule ICal.DeserializeTest do [event] = calendar.events - assert event.dtstart != nil - assert event.dtend != nil + assert event.dtstart == ~D[2026-05-29] + assert event.dtend == ~D[2026-05-30] end test "Bad separators do not disturb parsing" do