- iCal export: RFC 5545 text escaping for SUMMARY, DESCRIPTION and LOCATION,
line folding at 75 octets (UTF-8 aware), CRLF line endings, and
Content-Type text/calendar instead of the inferred text/html. A line break
in a description used to end the property early; the new calendar
frontend has a multi-line description field.
- formatDate no longer shifts the event's end date in place.
- GET /calendar/events/subscriptions: the iCal URL of every calendar, with
its shared password where needed, for the calendar app's "Abonnieren"
dialog. Editors only (requireAppAccess('calendar')), the shared password
is not accepted there, never cached; calendars without a configured
credential are left out.
- Tests for both; documented in docs/calendar-ical.md.
Backwards compatible with the current Angular calendar app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3.4 KiB
Calendar iCal export and subscriptions
How GET /calendar/events/{calendar}/ical builds its file, what changed on 2026-09-28, and the
endpoint the calendar app uses to hand editors ready-made subscription URLs.
The export
icalgenerator.service.ts turns a calendar's PUBLIC events into one VCALENDAR:
- Timed events:
DTSTART;TZID=Europe/Berlin:/DTEND;TZID=Europe/Berlin:formatted with the API process's localgetHours()etc. The times are therefore only right if the process runs in Berlin time (TZ=Europe/Berlin, or a server in that zone). Nothing enforces this. Inferred from the code; not checked against the production server. - Whole-day events:
VALUE=DATE, and the stored end date is treated as the inclusive last day - the generator adds one day forDTEND. The calendar app stores whole-day events as 00:00 to 23:59 Berlin time for this reason. repeatFrequencyis written verbatim asRRULE:FREQ=<value>(no interval, count or end).publicneeds no credential; the other calendars take?password=because a calendar client cannot send the session cookie (seecalendar-auth-migration.md).
RFC 5545 conformance (2026-09-28)
Before this date the generator wrote text values raw. A line break in a description ended the
DESCRIPTION property early and the rest was read as a garbage property; unescaped , and ;
are invalid in text values. The new calendar frontend has a multi-line description field, so this
went from theoretical to certain. Now:
SUMMARY,DESCRIPTIONandLOCATIONgo throughescapeText(\;,and line breaks, backslash first).URLis a URI value and is not escaped; line breaks are stripped from it.- Every content line is folded at 75 octets (UTF-8 aware - a multi-byte character is never split)
and the file uses CRLF line endings throughout (
toContentLines). formatDateno longer shifts the caller'sDatewhen it adds the whole-day end's extra day.- The response is
Content-Type: text/calendar; charset=utf-8. Before, Express inferredtext/htmlfrom the string body; calendar apps coped, but it was wrong.
Pinned by test/calendar/icalgenerator.service.test.ts.
Not changed, worth knowing: ORGANIZER is written as a bare display name (ORGANIZER:Anna),
while RFC 5545 expects a cal-address (ORGANIZER;CN=Anna:mailto:…). Calendar apps have tolerated
it so far.
GET /calendar/events/subscriptions
For the calendar app's "Abonnieren" dialog. Behind requireAppAccess('calendar'); the shared
password is not accepted here (one calendar's password must not reveal the others).
Returns [{calendar, icalUrl}] in calendarNames order, built from API_BASE_URL:
publicwithout a password;- every other calendar with
?password=<its shared password>, URL-encoded; - a calendar whose credential env var is unset is left out rather than listed with a URL that cannot work.
choir and birthdays share CHOIR_CREDENTIAL, so rotating it changes both URLs - and breaks
every existing subscription to either. The response is sent with Cache-Control: no-store
because it carries the passwords.
Signed-in editors can read every event of every calendar anyway, so handing them the passwords exposes nothing they could not already see.
Found while doing this
- Only the admin pool reads
DB_PORT;Calendar.db.ts(and the feedback/tickets pools) always connect to 3306. A local test database for the calendar has to listen there.