Subscribe to a calendar published at a URL

A timetable, a rota, a public holiday list: the calendars people are given
as a link, which ihasmail could not show at all.

Nothing is stored. The document is fetched when the calendar is opened and
parsed in the browser; the server keeps no copy, no cache and no schedule,
which is what lets an immutable container serve this. There is no timer
either -- there is nowhere to run one -- so the guarantee is that a
subscription is as current as the last time somebody looked, which is also
when it matters. That is said plainly rather than implied.

The fetch has to happen on the server: a calendar URL belongs to whoever
published it and almost none of them send CORS headers. That makes it the
second place this app knocks on a door somebody else chose, so the guard
the image proxy has always had was lifted out and both now call it. A
second SSRF implementation is how one of them ends up missing a case; this
way there is one, and the extraction is covered by the image proxy's own
tests still passing unchanged.

webcal: is understood, because that is how these are published, and it is
read as https: rather than waved past the checks -- a webcal URL pointing
at loopback is refused exactly like an http one.

Recurrence is deliberately not expanded. RRULE is a small language with a
lot of edge cases, and a subscription quietly showing the wrong dates would
be worse than one showing the first occurrence and saying so.

The parser is a subscription parser rather than an importer: a subscribed
calendar is read-only and redrawn from scratch each refresh, so nothing has
to round-trip or survive an edit, which is most of what makes a full
iCalendar implementation large. What it does have to do is never mis-state
a time -- a DATE is built in local time rather than at UTC midnight, which
would land on the day before for anyone west of Greenwich -- and never hang
on a document somebody else wrote.

Events go through instancesIn like the birthdays, so no view has to know
they are not real calendars, and the calendar they hang off reports no
write rights, so everything that asks before offering an edit declines on
its own. A subscription that cannot be read says so in the sidebar rather
than drawing an empty calendar, which looks like a calendar with nothing
in it.
This commit is contained in:
2026-09-02 00:52:06 -07:00
parent 0ac5f78789
commit 4d18d94b63
11 changed files with 885 additions and 25 deletions
+28
View File
@@ -529,6 +529,34 @@ nothing for anybody else.
- **iCal import** through `CalendarEvent/parse` (a file of any number of
events), from the calendar's own menu, into that calendar. The events are
filed rather than scheduled: no invitations go out to anyone named in them.
- **Subscribed calendars** by URL — a timetable, a rota, a public holiday list.
Added in Settings Calendar & contacts, read-only, and shown beside your own
with their own colour.
**Nothing is stored.** The document is fetched when you open the calendar and
parsed in the browser; the server keeps no copy, no cache and no schedule,
which is what lets an immutable container serve this at all. There is no
timer either: ihasmail has nowhere to run one, so the honest guarantee is
that a subscription is as current as the last time somebody looked — which is
also when it matters.
The fetch has to happen on the server, because a calendar URL belongs to
whoever published it and almost none of them send CORS headers. That makes it
the second place ihasmail reaches an address a stranger chose, and it goes
through **exactly the same guard as the image proxy** — one implementation,
not two: the name is resolved and every answer must be acceptable, the
connection is pinned to the address that was checked, and each redirect is
re-resolved and re-pinned. `webcal:` is understood, because that is how these
are published, and it is read as `https:` rather than waved past the checks.
Two consequences worth stating. A calendar on a private address — including
one on your own machine — is refused, by design. And **recurring events are
not expanded**: `RRULE` is a small language with a lot of edge cases, and a
subscription quietly showing the wrong dates would be worse than one showing
the first occurrence.
A subscription that cannot be read **says so** in the sidebar rather than
drawing an empty calendar, which looks like a calendar with nothing in it.
- **Birthdays**, as a calendar of its own derived from the birthdays already on
your contacts. Off until switched on in Settings Calendar & contacts, and
hideable from the calendar's own sidebar without turning it off.