github olivierlambert/calrs v1.19.0

3 hours ago

Minor release, mostly about reliability and language. Pending requests now hold their slot and can expire, so two guests can no longer request the same time (#234). Calendar write-back is verified: an event that never reached the host's calendar is retried, flagged on the dashboard and emailed to the host, and a calendar source that keeps failing alerts its owner (#162). Every email is written in its reader's language, and guest emails come from the host's name with a Reply-To to them (#209, #221). Dutch joins as the ninth locale. A red-team pass closed a booking-form bypass (a request sent straight to the booking URL could book a day the host had blocked), the last-admin lockout and missing server-side validation on event types, alongside some twenty other fixes. Six migrations (065 to 070), no required configuration change. Back up your database before upgrading and read the upgrade notes: /u/ pages now follow the visitor's language by default, and log targets are more specific.

Thanks to everyone who reported, tested and contributed to this one: @mdbraber, @Morthor, @Piergeek, @Nosochek, @hugo-fasone, @mkspflug, @joelgombin, @aburg, @gitwittidbit, @kosssi, @Fr0zenSide, @k00b0ld and @jptrmn.

Added

  • Pending requests hold their slot and expire (#234) - On event types that require confirmation, a pending request used to leave its time free: a second guest was offered the same slot, filled in the form and was only turned away at submit, and an overlapping slot on another of the host's event types could be requested too. A pending request now blocks its host's (or assigned member's) time across all event types, exactly like a confirmed booking. Requests that wait too long are declined on their own, never accepted: the guest gets an email (and an SMS when the event type collects phones) asking them to pick another time, with a link back to the booking page, and the host gets a short notice. Halfway to the deadline the host gets one reminder with the Approve/Decline buttons. Both settings sit under "Requires confirmation" in the event type form: "Pending requests block the time slot" (on by default) and "Request expiry" (never, 4, 24, 48 or 72 hours, 24 for new event types). The deadline never runs past the last moment another guest could still book the slot (start minus the minimum notice), except for a request made inside that window, which keeps its expiry up to the meeting start instead of lapsing within minutes; a guest reschedule that puts a booking back to pending restarts it. Reported by @Piergeek.

  • Approval re-checks the host's calendar (#234) - Approving a request, from the dashboard or from the email link, now refuses when the host (or the assigned member, or any member of a collective booking) has since become busy at that time, through a calendar event or another booking, instead of double-booking them.

  • Public booking page language - A new Profile & Settings choice decides the language of your public pages under /u/{username} and on the legacy /{slug} routes (profile, slots, booking form) and therefore of the emails and SMS your guests receive: "Each visitor's language" (the default: the visitor's browser language, as on team pages; never a logged-in user's saved preference, so an admin impersonating a user or staff signed in to calrs do not stamp their saved language on a guest's booking) or "Always my language" (the language you picked in the same settings, the behaviour introduced in #76).

  • Self-hosted captcha WASM (#164) - The captcha card in the admin panel has a new optional "Widget WASM URL". Even with a self-hosted Cap server and widget script, the widget fetched its proof-of-work solver from cdn.jsdelivr.net; pointing this field at the WASM file your Cap server serves keeps booking pages free of third-party requests. The URL's origin is added to the booking form's connect-src.

  • Dutch translation (#224) - calrs now ships in Dutch, the ninth locale. Thanks @mdbraber!

  • Booking form pre-filled for signed-in users (#226) - When a signed-in user opens a booking form on a /u/ page, their name and email are filled in. A personalised invite's guest details still take precedence. Thanks @Morthor!

Changed

  • Every email is now translated (#209) - Host, team member and watcher emails (new booking, approval request and its reminder with the Approve/Decline buttons, confirmation, reminder, cancellation, reschedule request, expired request, Google Calendar sync failure, claim offer and claim confirmation) were still English. Each is now written in the recipient's saved language from Profile & Settings, English when none is set; a collective booking emails every member in their own. The guest emails that were still English follow the language the guest booked in: the host-initiated "pick a new time" email, the reschedule notification, the copy sent to additional attendees, and the reason given when a host deletes the booking from their calendar. Invite emails follow the inviter's language. The "Sent by calrs" footer is translated everywhere. Host email subjects now separate the event and the guest with a comma instead of a dash, and detail labels carry the same colons as the guest emails. The admin SMTP test email stays English. Thanks to @kosssi for opening #209 and to @Piergeek for the precise report of what was left. Guest emails always use the language the booking form was shown in, so the page, the emails and the SMS agree; a guest cancelling or rescheduling from a browser in another language sees those pages, and gets the emails, in the language they booked in. Error pages from a booking use the booking page's language too. The host's approve and decline pages use the host's saved language, and claim pages the watcher's, like the emails that link to them.
  • Guest emails come from the host (#221) - Emails sent to guests (confirmation and the copies to additional attendees, pending, reminder, cancellation, decline, expired request, pick a new time, reschedule, invite) now show the host's name as the sender, followed by the configured sender name and joined in the email's language (Bob Smith via Acme Scheduling, or just Bob Smith when no sender name is set), and carry a Reply-To pointing at the host's booking email (the account email for invites, since their recipients are chosen by the sender). The sending address is unchanged, so SPF, DKIM and DMARC keep aligning with the instance domain. Round-robin bookings name the assigned member and collective bookings the whole roster, with replies going to the ICS organizer; this also fixes guest emails that named the event type owner or a single member instead: on round-robin bookings the cancellation a guest triggers and the decline sent from the email link, on collective ones the reminder and the expired-request email. Host-facing emails are unchanged. Thanks to @gitwittidbit for the suggestion.
  • Dutch: "Embed" instead of "Insluiten" (#227) - The embed button on the event types page keeps the English word, as Dutch web usage does. Thanks @mdbraber!
  • Source split into modules (contributors only) - src/web/mod.rs (39k lines) and src/email.rs (8k lines) are now src/web/ and src/email/, one file per feature area, with their tests under src/web/tests/ and src/email/tests/. A pure move: no behaviour change and the same tests. If you have an open pull request touching either file, rebase it; ARCHITECTURE.md (new) says which file now owns what.

Upgrade notes

  • Back up the database before upgrading. Migrations 065 to 070 add columns and settings; none rewrites existing data, but an older binary does not know them, so to roll back restore the pre-upgrade backup along with the old binary.
  • /u/{username} pages now follow the visitor's language by default. Since #76 they used the host's saved language. Migration 066 starts everyone, existing users included, on the visitor's language; hosts who want the previous behaviour can pick "Always my language" under "Public booking page language" in Profile & Settings.
  • Migration 065 adds the new settings. Existing event types get the slot hold on and no expiry, so requests that are already waiting are never declined by the upgrade; pick an expiry in the event type form to opt in.
  • Migration 067 adds the optional captcha WASM URL. Nothing changes until it is set.
  • Migrations 068 and 069 add write-back failure tracking to bookings and sync failure tracking to calendar sources (#162). No configuration change.
  • Migration 070 adds the stored organizer to bookings (#161). Only new collective team and dynamic group bookings fill it; existing bookings are not backfilled and keep resolving their organizer from the current roster, as before. No configuration change.
  • Log targets are more specific. With src/web/mod.rs and src/email.rs split into modules, log lines now carry the module that wrote them, e.g. calrs::web::booking, calrs::web::booking_actions, calrs::web::admin, calrs::email::transport instead of calrs::web / calrs::email. RUST_LOG filters such as calrs::web=debug still match (they match by prefix), but tooling that matches the exact target text (a fail2ban filter on calrs::web: rate limited, a log alert) needs updating.

Fixed

  • Collective bookings could change organizer when the team changed (#161) - A collective team booking has no assigned member, so its calendar organizer (the first eligible member by name) and the name and Reply-To on the guest's emails were recomputed from the team's current roster every time. Adding, removing, disabling or renaming a member, or changing a per-event-type weight, could make someone else the organizer of an existing event: the next approval or reschedule then pushed an event with a different ORGANIZER, which Google CalDAV refuses with 403 Forbidden, and the guest's later emails replied to a different person than the confirmation. Collective and dynamic group (/u/alice+bob/...) bookings now store the organizer they were created with, and every later push (approval, reschedule, claim, write-back retry, Google Meet) and guest email uses it, even if that member has since left. Dynamic group bookings also keep every host's name in the organizer and on the guest's later emails, which used to drop back to the first host alone. The guest's cancellation, decline and "pick a new time" emails sent when a host acts from the dashboard (or calrs booking cancel, or a sync that finds the event deleted) used to name whoever acted, so their cancellation invite carried a different ORGANIZER than the original, which calendar clients can ignore, leaving the event in the guest's calendar. They now name the booking's organizer too; the host's own copy is unchanged. Thanks to @hugo-fasone for the report and the analysis.
  • A rejected settings save could still change the username (#208) - In Profile & Settings, a new username was written before the booking email was checked, so a save refused for a malformed booking email still changed the username (and with it the public /u/ links). The username is now saved together with every other field, or not at all.
  • EWS cancel and reschedule could leave the event in Exchange (#179) - Some Exchange versions refuse to search a calendar by calendar:UID and answer ErrorUnsupportedPathForQuery, so calrs could not find the booking's item to delete it. On that refusal calrs now scans the calendar over a window from 30 days back to 400 days ahead and matches the UID itself. Thanks to @mkspflug for the report and the workaround this builds on.
  • Google Calendar setup named the wrong API (#194) - The setup guide and the admin panel hint said to enable the "Google Calendar API", but CalDAV access needs Google's separate "CalDAV API". Both now list the two APIs: the CalDAV API for busy times and write-back, the Google Calendar API for Google Meet links.
  • Calendar write-back failures are no longer silent (#162) - A booking was confirmed to the guest even when the event never reached the host's calendar (a refused PUT, or a server that answered 2xx and kept nothing), and only a log line said so. calrs now reads each written event back by UID and treats a write it cannot read back as failed. The booking stays confirmed (the slot is already blocked by calrs and the guest has the invitation), but the failure is recorded: the bookings dashboard shows a "not in your calendar" badge with the error, the background task retries it with backoff (5 min up to 8 h, seven retries, confirmed future bookings only, never duplicating the event), and after three failed attempts the host gets one email in their language asking them to add the event by hand.
  • Failing calendar sources are no longer silent (#162) - When a source could not be synced, availability kept being served from the stale cache with no signal, and calrs sync printed "Sync complete." after a failure. Slots are still served from the last good sync, but the calendar sources page now shows the error and since when, the owner is emailed once (in their language) when a source has failed at least 3 times over more than an hour, and again only after it recovered and failed again. calrs sync lists the failed sources and exits with status 1, a calendar that fails to fetch fails its source's sync instead of being skipped quietly, slot pages retry a failing source at most every five minutes, and one failing source no longer starves the others in the background sync. Thanks to @Nosochek for the report and the detailed analysis, including a working local fix and the Yandex repro.
  • Event-type cards broke when a description contained a link (#239) - On public profile and team pages, a Markdown link in an event type description nested one link inside another, which browsers split apart, breaking the card. The card and the description link are now separate. Thanks @Fr0zenSide for the report.
  • Not every available day showed on the first slot page load (#228) - The month view fetched slots for a window that could stop before the end of the visible month, so days near the end stayed empty until the visitor changed month. The week view also skipped or repeated weeks when navigating across a month boundary, and its forward arrow ignored the booking horizon. Thanks @k00b0ld for the report.
  • An unreachable SMTP server could hang a booking (#236) - Emails were sent inline, so a booking request waited for SMTP and could time out at the proxy (504) while the booking was already stored, and send failures were barely logged. Emails are now sent in the background with connect and send timeouts, drained on shutdown, and every failure is logged once with the recipient and server. Thanks @jptrmn for the report.
  • The guest's note and phone were missing from the dashboard (#230, #231) - The note was only in the notification email, and the phone was hidden until the booking was confirmed. Both now show on the bookings page and on pending requests. Thanks @Piergeek for both reports.
  • Approving from the dashboard skipped the SMS and watchers - A booking approved from the dashboard did not text the guest or notify watching teams, unlike the email approval link. Both paths now behave the same.
  • Concurrent approve, decline and cancel actions - Two people acting on the same booking at once could email the guest twice, or decline a booking that had just been approved. Every status change now only applies to the status it read.
  • The background calendar sync almost never ran - The 60-second loop skipped its sync step whenever no reminder was due. It now runs every time.
  • Calendar names from Fastmail showed <![CDATA[...]]> (#225) - CalDAV property values are now unwrapped from CDATA and their XML entities decoded, so "Sales & Marketing" reads "Sales & Marketing". Sync tokens containing & are escaped again when sent back. Thanks @mdbraber!
  • The EWS UID scan could miss an item on a busy calendar (#179) - Exchange cuts a CalendarView off at 1000 entries without saying so, so on a calendar that busy the fallback scan could miss the booking's item and report it as gone, leaving a cancelled booking's event in place. A window that comes back at the cap is now split in half and both halves scanned again, down to one day; a one-day window still at the cap is reported as an error instead of "not found".
  • Saving the captcha settings could wipe stored values - While the captcha was not fully configured (for example before the secret was entered), the admin form showed the instance URL, site key, widget URL and WASM URL empty, so the next save cleared them. The form now shows what is stored, whether or not it adds up to a working configuration. The captcha fields are also labelled for screen readers now.
  • A guest cancelling a round-robin booking notified the wrong host - The "booking cancelled" notice went to the event type's owner instead of the team member the booking was assigned to, in the owner's language, and the cancel page named the owner as the host. It now goes to the assigned member, in their language, and the page names them. On a collective booking it goes to every member hosting it, each in their own language, as the new-booking notice does. Personal bookings still notify the owner.
  • A booking could be made on a day the host had blocked - The slot page applied date overrides (a day off, or custom hours for one day), the weekly hours, the slot grid and "first slot only", but the booking form submission only checked busy times, minimum notice and the horizon. A request sent straight to the booking URL could therefore book a blocked day, a time outside a day's custom hours or the weekly hours, or a time off the slot grid, and it was confirmed. Every booking route (/u/, legacy /{slug}, team round robin and collective, dynamic group links, invite links) and the guest reschedule now recompute the slots the page would show, with the same code, and refuse a time that is not one of them.
  • The only admin could demote or disable themselves - In the admin panel, the last admin could take away their own admin role, or disable their own account, leaving the instance with no one able to reach the admin panel. Demoting, disabling and deleting now refuse to remove the last enabled admin (a disabled admin does not count, since it cannot sign in), with a message in the admin's language. The check is part of the database update itself, so two admins demoting each other at the same moment cannot both succeed. calrs user demote and calrs user disable apply the same rule.
  • Event types could be saved with an empty title or a negative duration - The event type form only checked its fields in the browser, so a request sent directly could create or edit an event type with an empty title, a duration of -5 minutes, or negative buffers, notice or reminder. A negative booking horizon was quietly stored as "no limit", and on edit a slug was saved with whatever characters it was given. Personal and team event types are now checked on the server when created or edited: a title is required (255 characters at most), the duration must be 1 minute to 24 hours, buffers, notice, reminder and additional guests must be 0 or more, and the horizon 0 days or more. The slug is still lowercased with spaces turned into dashes, and derived from the title when left blank, but any other character outside letters, digits and dashes is now refused with a message instead of being silently dropped on create and kept on edit, as for team slugs. The form comes back with the error and what was typed.
  • Some names could not be emailed - A host or guest whose display name contained @, a comma, quotes or angle brackets ("Alice @ Acme", "Dr. A, PhD") got no email at all, because the recipient was built as text and that text did not parse as an address. Names are now passed to the mail library as names, which quotes them as needed.

Don't miss a new calrs release

NewReleases is sending notifications on new releases.