github olivierlambert/calrs v2.0.0-beta.3
calrs 2.0.0-beta.3

pre-release50 minutes ago

The 2.0 features that waited in pull requests land: native Microsoft 365 and Outlook.com calendars through Microsoft Graph (#229, PR #290; thanks to @adeg, who wrote it and offered to maintain the provider), organization-wide Exchange calendars through impersonation (#138), Google out of office (#240), MCP over HTTP with OAuth (#273), a REST API with an OpenAPI reference (#169), booking webhooks (#168), booking questions (#210), host notes (#173), time off (#46), a write calendar per event type (#47) and a guest email footer (#223). Bookings are now checked one after the other under a lock, and an availability check that fails refuses the booking instead of treating the host as free. Twenty-three migrations, and a few changes of behaviour: read the upgrade notes below. Back up your database before upgrading, as for any pre-release.

Docker: this version is published as :2.0.0-beta.3 and :next. :latest stays on the 1.23 line until 2.0 is final.

Thanks to @adeg, @chetan, @huntervcx and @optimistic5 for the pull requests, and to @threema-danilo, @gounthar, @baptistebca, @alexture, @WEBDIENSTE and @Martin-Hausleitner for the requests and examples. Please field-test on a copy of your database and tell us what breaks.

Added

  • Booking questions (#210, #170) - An event type can ask guests more than their name and email: a Booking form section in the event type form adds questions of five kinds (short text, long text, one choice, several choices, checkbox), each required or optional, with a help text, in the order you set (Move up, Move down). A checkbox can record a consent (a privacy policy, a recording, a newsletter) with a policy link; an unticked optional box is kept as "no", and a consent box is never ticked in advance. The notes field gets its own setting: optional (as before), required or hidden. Answers are stored with the booking along with the question as the guest read it, so editing or deleting a question never rewrites a past booking. They are shown on the bookings page and the booking's notes page, returned by the REST API as answers, in the host emails that show the notes (new booking, approval request and reminder, booking confirmed, host reminder, reschedule request), on the guest's confirmation page and in their confirmation and pending emails, in booking webhooks (booking.answers, keyed by a stable key), and in the event calrs writes to the host's calendar, except questions marked to stay out of it (for sensitive answers delegates should not read). Never in email subjects, the emailed .ics, additional attendees' emails, shared resource calendars, SMS or logs. Limits: 20 questions, 30 choices per question. Duplicate copies the questions. Whoever may edit the event type edits its questions. Thanks @WEBDIENSTE for the detailed examples, @alexture for the consent checkboxes, @Martin-Hausleitner (#170) and @optimistic5 (#232); please field-test it. One migration (20261108101918_booking_questions).

  • Microsoft 365 / Outlook through Microsoft Graph (#229) - Delegated OAuth with PKCE, calendar discovery and selection, busy-time synchronization, and booking creation, rescheduling, and cancellation. Recurring occurrences and moved exceptions synchronize with their original recurrence identity, which Graph omits unless explicitly requested. Personal Microsoft and organizational accounts use the same native provider. Microsoft Exchange on-premises (EWS) remains a separate backend. A calendar a colleague shared, which Exchange Online lists and serves as one of the user's own once accepted in Outlook, does not block the user's availability unless an event type selects it (the event type form now lists every synced calendar), and read-only calendars are not offered for write-back; one already selected stays selected, flagged on the sources page, and its writes fail until another is chosen. The calendar selection per source (#324) works for Microsoft sources too. The overview's calendar link opens Outlook on the web. Until a Microsoft source completes its first sync of the calendars that matter (the write calendar and those that count for availability), and past 730 days after its last complete sync, the host counts as busy rather than free, which the sources page, the troubleshoot view and a refused approval say; a calendar that counts for availability but can no longer be read, or a write calendar Microsoft no longer lists, fails the source's sync so the owner hears of it; a booking whose Outlook event vanished from the calendar it was written to is cancelled only once Graph finds it in none of the account's calendars, shared ones included (an event moved to another calendar keeps it, and any doubt cancels nothing, reported as a sync failure once it lasts a day). A suspended user's Microsoft sources are neither synced nor have their access refreshed until they are re-enabled. An administrator registers the application under Admin > Sign-in and security; see the Microsoft calendar guide. Thanks @adeg.

  • Calendar OAuth provisioning CLI - calrs config calendar-oauth --provider google|microsoft reads credential JSON from standard input, encrypts the secret, and keeps it out of configuration dumps. Thanks @adeg.

  • Google out-of-office blocks availability (#240) - An "Out of office" event in a connected Google calendar now blocks booking slots. calrs reads them through the Google Calendar API, the documented interface for them, with the existing OAuth connection (no new scope, no reconnect), on every sync of the primary calendar, from 90 days back to 366 days ahead, with recurring absences expanded by Google, and whole-day ones blocking their days in the calendar's timezone. They follow the calendar's busy toggle, the "sync this calendar" switch and per-event-type calendar selection like any other event, and show only as busy time. If the API call fails, the absences cached by the last good sync keep blocking for up to 7 days, and the source shows as failing so its owner is told. Needs the Google Calendar API enabled in the Google Cloud project, as for Meet. Migration 20261108065837_google_out_of_office. Thanks @chetan.

  • Outgoing webhooks for booking events (#168) - Event types → Webhooks (/dashboard/webhooks) adds a URL that calrs POSTs a versioned JSON message to ("version": 1) when a booking is created, requested, confirmed, declined, expires, is cancelled (with who cancelled) or rescheduled (with the old times). A webhook covers one event type (managed by whoever may manage it: the owner, the team's admins) or all of a user's personal event types. Every request is signed with the endpoint's secret, HMAC-SHA256 of the raw body in X-Calrs-Signature like the meeting webhook, and the body carries a per-attempt timestamp and an idempotency id, so receivers can refuse replays and duplicates; the secret is encrypted at rest and kept when the edit field is left empty. Deliveries are queued after the booking commits and sent by a background task: a webhook never slows down or fails a booking. Failures are retried after 1, 5, 15 and 60 minutes, 4 and 12 hours, and the last result, error and queue are shown per webhook, with a "Send test" button. URLs must be https (http only for a host on the private-host allowlist), carry no credentials and resolve to public addresses, checked again at every delivery with the checked addresses pinned for the connection; redirects are not followed and at most 200 characters of an error body are kept. Each webhook picks its events (all by default). A webhook on an event type outlives the user who added it. "Send test" is limited to 10 per user per 10 minutes. Admin-only by default: Admin panel → Email and SMS → Booking webhooks lets every user add them. Payload and verification examples: docs/src/webhooks.md. One migration.

  • Write confirmed bookings to a calendar per event type (#47) - Until now every confirmed booking went to the write calendar of each of the host's sources. An event type can now send its bookings to a calendar of its own, for instance coaching into a personal calendar and team calls into a shared work calendar: "Write confirmed bookings to" in the event type form, and a "Bookings by event type" section on the Calendar sources page that lists every event type you host. On a team event type each member chooses their own calendar, since the booking lands in the assigned member's (or each collective member's) calendar. Only your own calendars can be chosen. Without a choice nothing changes. A booking follows a changed choice at its next write (reschedule, approval, retry, claim), written there before the earlier copy is removed, and is deleted from wherever it was written; a disabled or removed source, or a chosen calendar that refuses the booking for good (deleted or read-only), falls back to the source setting. Google Meet events are created on the chosen calendar when it is a Google one and never move afterwards. Thanks @gounthar and @baptistebca for the request.

  • Host notes on a booking (#173) - The hosts of a booking can keep notes about it (what was said on a call, what to prepare), separate from the guest's own notes: the booking's title on the bookings page or the overview opens /dashboard/bookings/{id}/notes; a row shows an Edit note button (or, on the overview agenda, a quiet "Host note" marker) only once a note exists. Readable and editable by the event type owner, the member a round-robin booking is assigned to, every member of a collective booking (except excluded members) and the watcher who claimed it, following the team's current members; anyone else gets the dashboard's 404 page. A save made while an admin impersonates a host records the admin, "as" that host. Shown on the dashboard and in the host emails sent afterwards (reminder, approval reminder, booking confirmed, cancellation, reschedule request), and returned by the REST API as host_notes to the hosts who may read them, never to the guest: not on any guest page, in guest emails, SMS, .ics files, webhooks or the calendar event calrs writes. A save made from a stale page is refused instead of overwriting another host's edit (a version counter), and shows the version saved meanwhile. Multi-line notes keep their line breaks in HTML emails. Notes survive reschedules and cancellations, and the bookings page lists the bookings due in the last 30 days (held, cancelled or declined) so hosts can add notes after the call. Each host email includes the notes only if its recipient may still read them. Migration 20261108032313_booking_host_notes.

  • Members' time off on the team settings page (#46) - Team admins see a read-only Time off section on /dashboard/teams/{id}/settings listing the upcoming time off of the team's members: the member's name and the dates and times in the admin's own time zone, never the note the member gave the period. Members of the team and other users do not see it, and it is absent when no member has time off planned. The Time off page tells users so, under the note field.

  • Cancelled bookings on the bookings page (#173) - A Cancelled and declined section lists the bookings you host that were cancelled or declined, future ones and those due in the last 30 days, each title opening the booking's host notes, so the notes of a meeting cancelled for a later date stay reachable. It follows the host-notes rule: a booking you do not host never appears. Last 30 days now lists the meetings held, so a cancelled one is listed once. Absent when there is nothing to list.

  • Time off (#46) - A Time off page in the dashboard (and calrs user time-off in the CLI) holds the periods you take no bookings: whole days, or part of a day with a start or end time, in your profile time zone, with an optional note only you see. Every event type you host follows them: personal ones lose the slots, round robin skips you and your teammates still take the bookings, collective loses the slot. The booking check, reschedules and approvals refuse the period too, the troubleshoot view shows it as "Time off" with your note, and the overview no longer counts it as open hours. Guests only see that the times are not available. Bookings already in a new period are kept, not cancelled. Ended periods are deleted after 30 days. New migrations 20261107214059_user_time_off and 20261108005717_booking_participants: a dynamic group booking (/u/alice+bob/...) now remembers its co-hosts, so rescheduling or approving it checks that every one of them is free, not only the first.

  • Guest email footer (#223, #350) - Admins can set a short footer (bold, italic and links, 500 characters at most, one text for all languages) added to every email a guest or an additional attendee receives: confirmation, reminder, cancellation, pending notice, decline, expiry, "pick a new time", reschedule and invite. An event type can replace it with its own. Host, watcher and claimant emails never carry it. Only admins write footers, unless an admin turns on "Let all users set a footer on their own event types" (separate from the SMS policy). Two migrations (20261107192406_guest_email_footer, 20261107212745_claimant_attendee). Thanks @optimistic5.

  • REST API (#169) - A small JSON API under /api/v1 for integrations: list the event types you manage (and the internal ones the directory offers you, read-only), read their bookable times over up to 31 days in any timezone, read the bookings you host, book an event type you manage for a guest, cancel a booking and ask the guest for a new time. Slots come from the booking page's own engine, and writes go through the booking form's and the dashboard's own code, refactored into shared functions with the web pages unchanged: a booking made through the API is validated, re-checked under the booking lock, made pending or confirmed, emailed, written to calendars and texted like one made on the booking page, with two differences, since it is the host booking for the guest: it asks none of the event type's booking questions (required consents included), and it keeps the guest's notes even when the event type hides the notes field. A guest phone stored before the event type stopped asking for one is returned as null, as every other channel hides it. Authentication is a Bearer token, never a cookie: OAuth applications get tokens for the new resource <base>/api/v1 from the server MCP uses (scopes event-types:read, slots:read, bookings:read, bookings:write, shown on the consent page and on Connected apps), and Profile & Settings → API tokens creates personal access tokens for scripts (shown once, stored hashed, chosen permissions, optional expiry, revocable). Only admins can use the API (personal tokens or OAuth grants for it) unless the admin turns on "Let all users use the REST API"; MCP is unaffected. API bookings and cancellations queue the same webhooks as the web. Bookings through the API are capped at 50 per user per day and 10 additional attendees each, since each one sends email. A second "pick a new time" request is refused while the guest has not answered the first (see the upgrade notes). Tokens follow the session rules: MFA policy, deleted on suspension, password or MFA changes, and by the admin's "Revoke connected apps". Errors have one shape ({"error": {"code", "message"}}), lists are cursor-paged, requests are rate limited per token and writes per user, and POST /api/v1/bookings accepts an Idempotency-Key. No endpoint returns credentials, booking tokens or calendar data. See the new "REST API" chapter of the docs. Migration 20261108093146_api_tokens.

  • OpenAPI document and interactive reference for the REST API (#169) - GET /api/v1/openapi.json serves an OpenAPI 3.1 description of the API, generated from the handlers themselves with utoipa (every operation, its parameters, request bodies, the answer of each status with the error codes it can carry, the Idempotency-Key and Retry-After headers, pagination, both Bearer schemes with the scope each operation needs), with the server and OAuth URLs of the instance. It is public, since it describes the API and holds no data. Tests fail when a route and the document disagree, when an operation documents another scope than its handler checks, or when an answer does not match its schema. /api/docs renders it for signed-in users with Swagger UI 5.34.1, vendored in the binary (Apache-2.0, about 1.9 MB, no CDN, no external font or request, under the usual Content-Security-Policy): "Try it out" works with a personal access token pasted under Authorize and never sends the session cookie. Linked from the API tokens page.

  • Organization-wide Exchange calendars through impersonation (#138) - An admin configures one Exchange (EWS) endpoint and one service account holding the ApplicationImpersonation role (Admin, Sign-in and security tab, Exchange card), and every user gets a managed calendar source that reads their own mailbox, with no credentials to enter. Optional: auto-provisioning, a lock on user-added sources, and a domain override for an AD whose login domain is not the mail domain. Hardened before release:

    • Only a trusted address gets a mailbox: an account created with calrs user create, an SSO account whose email the identity provider marks verified (checked again at every login, and a login that is not verified removes the managed source), or an account an admin confirmed, on the card or with the new calrs user trust-email <email>. Self-registered accounts never get one on their own, and accounts that exist before the upgrade start unconfirmed.
    • Registration now refuses an address that differs from an existing one only in letter case, and addresses are compared without regard to case when provisioning.
    • The domain override rewrites only addresses in the users' domain you name, an address without exactly one @ is never provisioned, and one mailbox serves at most one user (a conflict is shown in the status table).
    • Managed sources keep busy times only: no title, description, location or iCal text is stored for events from a managed mailbox, so admins cannot read mailbox content through calrs.
    • Suspended users, and every managed source while the feature is off, are neither synced nor written to, without counting as failures. "Remove managed sources and clear the service password" takes the feature out of service; bookings already written into mailboxes stay there.
    • The write calendar of a source must be one of its own calendars.
    • The global endpoint must be https, and saving a new endpoint or service account without retyping the password clears the stored one, so it is never sent to a server it was not typed for.
    • Addresses are compared as ASCII (a non-ASCII address names no mailbox), registration stores the address it checked, SSO linking by email ignores case, and an owner verified by the identity provider takes a mailbox over from an account that is only admin-confirmed.
    • A sync that started on a mailbox the source no longer reads stores nothing, calendars the mailbox no longer lists are dropped, and calendar folder names are replaced by neutral ones.
    • An admin can remove one user's managed source from the status table.
    • The status table shows 50 sources a page, failing ones first, with a search by user or mailbox once a list runs over one page; the addresses waiting for confirmation are paged too, so a large organization does not load every user on each visit to the admin panel.
    • An EWS calendar folder is only dropped after two listings in a row miss it, never on an empty or malformed answer (an HTTP 500 or a proxy's sign-in page now fails the sync); this also protects users' own EWS sources.
    • With an identity provider that does not send email_verified (ADFS, Entra ID by default), an admin's confirmation of an unchanged address survives SSO logins.
    • A confirmation covers exactly the address and mailbox the admin saw, and is refused if either changed since.
    • A new domain override that re-maps an admin-confirmed address asks for a new confirmation; only a new server, not a rewritten URL, resets the managed sources, and the card counts the event types that lost a calendar selection.

    Thanks @huntervcx. See "EWS Impersonation (Exchange)" in the docs.

  • MCP over Streamable HTTP, with OAuth (#273) - calrs serves a Model Context Protocol endpoint at /mcp, so an AI client (Claude, Cursor and the like) can list the event types you can distribute, read your own busy times and create single-use invite links, as you. The client registers itself and the user approves it on a consent page after signing in as usual (password, two-factor code, or SSO): no API token to copy, nothing to enable. It is a small first-party OAuth server: public clients, S256 PKCE only, exactly registered callbacks (loopback port relaxed per RFC 8252), single-use two-minute codes, hashed tokens, one-hour access tokens and rotating refresh tokens whose replay revokes the grant, and revocation. The busy-time tool returns merged anonymous intervals only, never event titles, and no tool books, cancels or emails anyone. Grants follow the session rules of local MFA (#213): the approval needs a fully signed-in session, a grant made without MFA stops working when the admin requires it, and suspending a user, resetting their password or their MFA, or changing their MFA deletes their grants along with their sessions. A Connected apps page in Profile & Settings lists the applications a user approved (permissions, approval day, last use) with a Revoke button, and the admin panel's user menu can revoke all of a user's connected apps. The OAuth endpoints are rate limited per IP. See the new "MCP integration" chapter of the docs. Two migrations. Thanks @chetan, and please field-test it with your MCP clients.

Changed

  • CalDAV responses are read with a real XML parser (#325) - The CalDAV client read server answers with a hand-written string search, which kept needing server-specific fixes (Kerio Connect's namespaces on <response> in #312, prefixes in #296, SOGo's in #15). It now parses them with quick-xml and matches elements by namespace URI, so any prefix, a default namespace, or a namespace declared on the element itself all read the same; calendar and calendar-data are recognised in any namespace, as some servers write them in the WebDAV one. Properties come only from a <propstat> whose status is 2xx. PHP warnings a Baikal or Nextcloud install appends after the answer are ignored. Answers from the servers calrs is used with (BlueMind, Nextcloud, Baikal, Radicale, SOGo, Kerio Connect, Zimbra, iCloud, Google, Fastmail) read as before. What changes: the PROPFIND and REPORT answers a sync acts on must now be a 207 (or a 200 carrying a multistatus) with a well-formed WebDAV multistatus body. Any other status, an empty body, malformed XML, or an HTML error page fails that calendar's sync with an error. Before, an empty 5xx body or a proxy's error page read as a calendar with no events: the cache survived, but the calendar's new ctag was stored, so its changes were missed until the next daily full resync. A rejected time-range or sync-token query still falls back as before. A sync-collection the server cuts short (a 507) is now continued on the next sync instead of the calendar being skipped as unchanged. Error messages give the status only, no longer the start of the server's answer, which on a shared calendar can quote other people's events. Bodies are read with a 64 MB cap as they arrive and parsed off the threads serving pages; the XML is limited to 128 levels of nesting, a million elements and 256 namespace declarations in scope, and entities are never expanded beyond the predefined ones. Thanks @threema-danilo for the report and the push to do it properly.
  • Booking directory scales to large instances (#340) - The directory's visibility rules, search and favorites filter now run in SQL, 50 meetings to a page with Previous and Next links once there are more, and the host search only reads the members of the meetings on the page. Search stays literal, case blind in every alphabet, and multi-word. The dashboard shows the first 12 favorites and links to the rest. Searches drop control characters and use at most 8 distinct words. A favorite still never grants access: every query applies the same checks. A directory that fits on one page looks exactly as before.
  • Admin Users tab and overview on large instances (#343) - Past 100 users, the Users tab searches, filters, sorts and pages on the server (25 rows per page, state in the URL), so 2,000 users no longer mean megabytes of HTML; promote, disable and delete return to the same page. The overview's booking figures (totals, weekly chart, rankings) are kept for a minute on instances with more than 20,000 bookings, with a note saying how old they are; user counts, calendar health and the attention list stay live. Smaller instances behave as before.

Fixed

  • A calendar Microsoft no longer lists is removed - A deleted calendar, or one whose share was removed, would stay in the cache and its events kept blocking slots. The next sync drops it with its events, except the source's write calendar. A calendar you turned off in the calendar selection is kept, with its setting, as long as Microsoft still lists it, and an empty listing removes nothing. EWS sources keep their own rule (#362). Thanks @adeg.
  • Microsoft snapshot reconciliation - Only events starting inside the fetched window are compared, as instants rather than date prefixes, and a vanished event cancels its booking only on the calendar it was written to. On Microsoft, an empty snapshot removes the events deleted since the last sync, and one unreadable item does not stop the rest of its calendar from syncing: the readable ones are cached and nothing is removed until a clean snapshot, and the source reports the sync as failing meanwhile: the sources page shows the error at once, and the owner is emailed once the source has failed at least three times over more than an hour, if SMTP is configured. EWS keeps its own sync path. EWS timestamps with fractional seconds (20230207T170000.000Z) now parse. Thanks @adeg.
  • Cancelling removes every host copy - With no destination chosen for its event type, a booking is written to the write calendar of each of the host's sources, but cancelling or rescheduling deleted only the one its stored calendar named, leaving the others in the host's calendars. Each copy is deleted now; copies recorded at a chosen destination (#47) are deleted where they were written, as before. A booking cancelled because its event was deleted from a calendar loses its other copies too (a claimant's, those at a destination, the host's other write calendars), as on any host cancellation.
  • The event type's calendar help said an empty selection checks every calendar; it checks the busy ones, which leaves out calendars colleagues shared. Corrected in every language.
  • No more duplicate invitations from Google Calendar (#237) - A booking written to a Google calendar reached the guest twice, calrs's invitation and Google's own, because Google's CalDAV ignores the SCHEDULE-AGENT=CLIENT marker other servers honour. On Google, the event is now written without attendees over CalDAV, and the guest and additional attendees are added through the Google Calendar API with sendUpdates=none, which Google documents as sending no notification. The guests stay on the event, so Google Meet lets them in without the "ask to join" lobby. Reschedules, Meet time changes and cancellations go the same silent way. Only one Google copy per booking lists the guests (the Google Meet owner's, else the host's), since Google adds the event to each attendee's own Google Calendar: the other members' copies of a collective booking and a claimant's copy carry none. Without the Calendar API enabled, a new event is written without attendees; when the API fails for a moment (rate limit, server error, no answer within 30 seconds) nothing is written and the write-back retries, as it does when the guests cannot be added back after a write. Other CalDAV servers are unchanged. Thanks @chetan.
  • Google Meet creation failures stop at once (#237) - calrs read the conference status from a field Google does not send, so a failed Meet creation was retried five times before giving up; it now reads conferenceData.createRequest.status.statusCode and stops on failure. Calendar API errors no longer carry Google's response body into the logs, and a refused Google token request is reported with its HTTP status and OAuth error code only. Thanks @chetan.
  • Bookings under concurrent load - Several booking requests arriving together could wait on each other for database connections for up to 30 seconds, and an availability check that failed was then treated as a free calendar. A booking now opens its database transaction only for its writes, after its checks, and every booking, guest reschedule and approval runs its checks and its write under one process-wide lock, so concurrent requests are checked one after the other and each sees the bookings made before it, on any event type of the host. Nothing under that lock waits on the network: resource feeds are refreshed before it, and read from the cache under it. An availability read that fails at booking time (calendar events, bookings, time off, working hours, Microsoft coverage, shared resources, date overrides and slot settings, the booking's hosts) now refuses the booking, reschedule or approval with the "try again" error page (a 500 through the API) rather than treating the host as free; slot pages keep showing what they can read and log the error, except that an unreadable list of required resources now hides the slots, as an unreadable resource calendar does.
  • Overview of team members - A round-robin booking with no assigned member (the member's account was deleted, or an old row) was listed on the overview of every member of the team, as if it were collective. It is now listed for the event type's owner only; collective bookings are unchanged.

Security

  • The private-address check missed most IPv6 private ranges (SSRF). It only matched fc00::/16 and fd00::/16, so a unique local address such as fd12:3456::1 passed as public, and only fe80::/16 of the link-local range. It now covers fc00::/7, fe80::/10, site-local fec0::/10, IPv6 multicast, IPv4 multicast, broadcast and 240.0.0.0/4, 0.0.0.0/8 and 198.18.0.0/15, and judges IPv4-mapped (::ffff:a.b.c.d), IPv4-translated (SIIT, ::ffff:0:a.b.c.d), IPv4-compatible (::a.b.c.d), NAT64 (64:ff9b::/96) and 6to4 (2002::/16) addresses by the IPv4 address they carry. Upgrade note: some addresses that used to pass now need the host on the private-host allowlist (CALRS_ALLOW_PRIVATE_HOSTS or Admin panel, System), notably 64:ff9b:1::/48 (RFC 8215 local-use NAT64) and 198.18.0.0/15, which is also the default fake-IP range of Clash and sing-box: a host resolving there through such a proxy is refused. Resource feeds re-check their address on every fetch, so an affected feed stops syncing until its host is allowlisted; CalDAV source URLs are checked when they are saved (added or edited), so existing sources keep syncing. Booking webhooks re-check before every delivery.
  • SSO linking no longer takes over an account through a recycled address - An SSO login with a verified address belonging to a calrs account already linked to another identity (an address the identity provider reassigned to a newcomer) linked the newcomer to that account, handing them the previous holder's calrs account. Linking by address now only applies to an account no identity is linked to yet; otherwise the login is refused and an administrator decides.
  • New users always get a scheduling account of their own - Under certain conditions, a new user (registration, single sign-on auto-registration or calrs user create) could be attached to an existing user's scheduling account, with its event types, bookings and calendars. A new user now always gets a new account. At startup, an account left without a user is only linked to a user with the same address who has no account yet, and gets no new user when its address matches an existing user's except for letter case or spaces; otherwise it stays unlinked and a warning names it in the log for the administrator to review. A single sign-on login that changes a user's address now also updates the copy kept on their account. Registration also stores the email address without surrounding spaces.

Upgrade notes

Back up your database before upgrading. Behaviour changes to know about:

  • Bookings take a lock and fail closed. Every booking, guest reschedule and approval runs its checks and its write under one process-wide lock, so concurrent requests are checked one after the other. An availability read that fails at booking time now refuses the booking (the "try again" page, a 500 through the API) instead of treating the host as free.
  • Admin-only by default: booking webhooks (Admin, Email and SMS, "Booking webhooks"), the REST API ("Let all users use the REST API") and the guest email footer on event types ("Let all users set a footer on their own event types"). An admin turns each on for every user. MCP needs no switch.
  • A second "pick a new time" request is refused while the guest has not answered the first one, on the dashboard and through the API. The first request can be resent, with the same link, at most once an hour.
  • Accounts that exist before the upgrade start unconfirmed for Exchange impersonation (#138): none gets a managed source until an admin confirms its address, or a verified SSO login vouches for it.
  • Some private IPv6 and IPv4 ranges now need the private-host allowlist (see Security below).
  • A CalDAV answer that is not a well-formed multistatus now fails the calendar's sync (#325) where an empty 5xx body or a proxy's error page used to read as a calendar with no events, so owners may get sync failure emails they did not get before.

Migrations, in the order they run:

  • 20261107192406_guest_email_footer: adds the guest email footer to event_types and auth_config, and auth_config.guest_footer_allow_all_users (off).
  • 20261107212745_claimant_attendee: adds booking_attendees.added_by_claim and claimant_user_id, and marks the attendee rows earlier claims added.
  • 20261107214059_user_time_off: creates user_time_off.
  • 20261108005717_booking_participants: creates booking_participants, the co-hosts of a dynamic group booking.
  • 20261108032313_booking_host_notes: adds bookings.host_notes with its version, time and author columns.
  • 20261108033131_event_type_write_targets: creates event_type_write_targets and booking_write_copies.
  • 20261108033132_write_target_refusals: adds the refusal columns to event_type_write_targets, bookings.caldav_source_id, delete retries on booking_write_copies, and a trigger that forgets a removed source's write-back location.
  • 20261108044050_oauth_server: creates the OAuth server tables (oauth_clients, oauth_requests, oauth_grants, oauth_credentials).
  • 20261108044051_oauth_credential_scope: adds oauth_credentials.scope.
  • 20261108044300_ews_global_config: adds the global Exchange settings to auth_config.
  • 20261108044301_caldav_sources_managed: adds caldav_sources.managed and impersonate_email.
  • 20261108044302_ews_impersonation_domain: adds auth_config.ews_impersonation_domain.
  • 20261108044303_managed_ews_unique: one managed Exchange source per account (unique index).
  • 20261108044304_ews_email_trust: adds users.email_trusted (every existing account starts unconfirmed) and auth_config.ews_impersonation_from_domain, and one mailbox per managed source (unique index).
  • 20261108044305_ews_idp_verified: adds users.email_verified_by_idp.
  • 20261108044306_ews_calendar_misses_takeover: adds calendars.ews_missing_count and the takeover columns of caldav_sources.
  • 20261108044519_booking_webhooks: creates webhooks and webhook_deliveries, and adds auth_config.webhooks_allow_all_users (off).
  • 20261108065837_google_out_of_office: adds events.google_out_of_office.
  • 20261108093146_api_tokens: creates api_tokens, api_idempotency and api_booking_log, and adds auth_config.api_tokens_allow_all_users (off) and bookings.reschedule_requested_at.
  • 20261108100216_microsoft_oauth: adds the Microsoft application settings to auth_config, caldav_sources.microsoft_user_id and the microsoft_oauth_states table.
  • 20261108100217_calendar_read_only: adds calendars.read_only.
  • 20261108100218_orphan_check_inconclusive: adds caldav_sources.orphan_check_inconclusive_since.
  • 20261108101918_booking_questions: creates event_type_questions and booking_answers, and adds event_types.notes_mode (optional, as before).

Don't miss a new calrs release

NewReleases is sending notifications on new releases.