Add multi-tenant organization model populated from OIDC claims with org-scoped user discovery, CalDAV principal filtering, and cross-org isolation at the SabreDAV layer. Add bookable resource principals (rooms, equipment) with CalDAV auto-scheduling that handles conflict detection, auto-accept/decline, and org-scoped booking enforcement. Fixes #14. Replace CalendarSubscriptionToken with a unified Channel model supporting CalDAV integration tokens and iCal feed URLs, with encrypted token storage and role-based access control. Fixes #16. Migrate task queue from Celery to Dramatiq with async ICS import, progress tracking, and task status polling endpoint. Replace nginx with Caddy for both the reverse proxy and frontend static serving. Switch frontend package manager from yarn/pnpm to npm and upgrade Node to 24, Next.js to 16, TypeScript to 5.9. Harden security with fail-closed entitlements, RSVP rate limiting and token expiry, CalDAV proxy path validation blocking internal API routes, channel path scope enforcement, and ETag-based conflict prevention. Add frontend pages for resource management and integration channel CRUD, with resource booking in the event modal. Restructure CalDAV paths to /calendars/users/ and /calendars/resources/ with nested principal collections in SabreDAV.
196 lines
7.0 KiB
Markdown
196 lines
7.0 KiB
Markdown
# Invitations
|
|
|
|
How event invitations work end-to-end: creating, sending, responding,
|
|
updating, and cancelling.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Frontend (EventModal)
|
|
→ CalDAV proxy (Django)
|
|
→ SabreDAV (stores event, detects attendees)
|
|
→ HttpCallbackIMipPlugin (HTTP POST to Django)
|
|
→ CalendarInvitationService (sends email)
|
|
→ Attendee receives email
|
|
→ RSVP link or iTIP client response
|
|
→ RSVPView (Django) or CalDAV REPLY
|
|
→ PARTSTAT updated in event
|
|
→ Organizer notified
|
|
```
|
|
|
|
## Creating an event with attendees
|
|
|
|
1. User adds attendees via `AttendeesSection` in EventModal
|
|
2. `useEventForm.toIcsEvent()` serializes the event with `ATTENDEE`
|
|
and `ORGANIZER` properties
|
|
3. `CalDavService.createEvent()` sends a PUT to CalDAV through the
|
|
Django proxy
|
|
4. The proxy (`CalDAVProxyView`) injects an
|
|
`X-CalDAV-Callback-URL` header pointing back to Django
|
|
|
|
The resulting `.ics` contains:
|
|
|
|
```ics
|
|
BEGIN:VEVENT
|
|
UID:abc-123
|
|
SUMMARY:Team Meeting
|
|
DTSTART:20260301T140000Z
|
|
DTEND:20260301T150000Z
|
|
ORGANIZER;CN=Alice:mailto:alice@example.com
|
|
ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:bob@example.com
|
|
SEQUENCE:0
|
|
END:VEVENT
|
|
```
|
|
|
|
## SabreDAV processing
|
|
|
|
When SabreDAV receives the event, three plugins run in order:
|
|
|
|
1. **CalendarSanitizerPlugin** (priority 85) — strips inline binary
|
|
attachments (Outlook signatures), truncates oversized fields,
|
|
enforces max resource size (1 MB default)
|
|
2. **AttendeeNormalizerPlugin** (priority 90) — lowercases emails,
|
|
deduplicates attendees keeping the highest-priority PARTSTAT
|
|
(ACCEPTED > TENTATIVE > DECLINED > NEEDS-ACTION)
|
|
3. **iMip scheduling** — detects attendees and creates a REQUEST
|
|
message for each one
|
|
|
|
The scheduling message is routed by **HttpCallbackIMipPlugin**, which
|
|
POSTs to Django:
|
|
|
|
```
|
|
POST /api/v1.0/caldav-scheduling-callback/
|
|
X-Api-Key: <shared secret>
|
|
X-CalDAV-Sender: alice@example.com
|
|
X-CalDAV-Recipient: bob@example.com
|
|
X-CalDAV-Method: REQUEST
|
|
Content-Type: text/calendar
|
|
|
|
<serialized VCALENDAR>
|
|
```
|
|
|
|
## Sending invitation emails
|
|
|
|
`CalDAVSchedulingCallbackView` receives the callback and delegates to
|
|
`CalendarInvitationService.send_invitation()`.
|
|
|
|
Steps:
|
|
|
|
1. **Parse** — `ICalendarParser.parse()` extracts UID, summary,
|
|
dates, organizer, attendee, location, description, sequence number
|
|
2. **Template selection** based on method and sequence:
|
|
| Method | Sequence | Template |
|
|
|--------|----------|----------|
|
|
| REQUEST | 0 | `calendar_invitation.html` |
|
|
| REQUEST | >0 | `calendar_invitation_update.html` |
|
|
| CANCEL | any | `calendar_invitation_cancel.html` |
|
|
| REPLY | any | `calendar_invitation_reply.html` |
|
|
3. **RSVP tokens** — for REQUEST emails, generates signed URLs:
|
|
```
|
|
/rsvp/?token=<signed>&action=accepted
|
|
/rsvp/?token=<signed>&action=tentative
|
|
/rsvp/?token=<signed>&action=declined
|
|
```
|
|
Tokens are signed with `django.core.signing.Signer(salt="rsvp")`
|
|
and contain `{uid, email, organizer}`.
|
|
4. **ICS attachment** — if `CALENDAR_ITIP_ENABLED=True`, the
|
|
attachment includes `METHOD:REQUEST` for iTIP-aware clients
|
|
(Outlook, Apple Mail). If False (default), the METHOD is stripped
|
|
and web RSVP links are used instead.
|
|
5. **Send** — multipart email with HTML + plain text + ICS attachment.
|
|
Reply-To is set to the organizer's email.
|
|
|
|
## Responding to invitations
|
|
|
|
Two paths:
|
|
|
|
### Web RSVP (default)
|
|
|
|
Attendee clicks Accept / Maybe / Decline link in the email.
|
|
|
|
`RSVPView` handles `GET /rsvp/?token=...&action=accepted`:
|
|
|
|
1. Unsigns the token (salt="rsvp")
|
|
2. Finds the event in the organizer's CalDAV calendar by UID
|
|
3. Checks the event is not in the past (recurring events are never
|
|
considered past)
|
|
4. Updates the attendee's `PARTSTAT` to ACCEPTED / TENTATIVE / DECLINED
|
|
5. PUTs the updated event back to CalDAV
|
|
6. Renders a confirmation page
|
|
|
|
The PUT triggers SabreDAV to generate a REPLY message, which flows
|
|
back through HttpCallbackIMipPlugin → Django → organizer email.
|
|
|
|
### iTIP client response
|
|
|
|
When `CALENDAR_ITIP_ENABLED=True`, email clients like Outlook or
|
|
Apple Calendar show native Accept/Decline buttons. The client sends
|
|
an iTIP REPLY directly to the CalDAV server, which triggers the same
|
|
callback flow.
|
|
|
|
## Updating an event
|
|
|
|
When an event with attendees is modified:
|
|
|
|
1. `CalDavService.updateEvent()` increments the `SEQUENCE` number
|
|
2. SabreDAV detects the change and creates REQUEST messages with the
|
|
updated sequence
|
|
3. Attendees receive an update email
|
|
(`calendar_invitation_update.html`)
|
|
|
|
## Cancelling an event
|
|
|
|
When an event with attendees is deleted:
|
|
|
|
1. SabreDAV creates CANCEL messages for each attendee
|
|
2. Attendees receive a cancellation email
|
|
(`calendar_invitation_cancel.html`)
|
|
|
|
## Configuration
|
|
|
|
| Setting | Default | Description |
|
|
|---------|---------|-------------|
|
|
| `CALDAV_URL` | `http://caldav:80` | Internal CalDAV server URL |
|
|
| `CALDAV_INBOUND_API_KEY` | None | API key for callbacks from CalDAV |
|
|
| `CALDAV_OUTBOUND_API_KEY` | None | API key for requests to CalDAV |
|
|
| `CALDAV_CALLBACK_BASE_URL` | None | Internal URL for CalDAV→Django (Docker: `http://backend:8000`) |
|
|
| `CALENDAR_ITIP_ENABLED` | False | Use iTIP METHOD headers in ICS attachments |
|
|
| `CALENDAR_INVITATION_FROM_EMAIL` | `DEFAULT_FROM_EMAIL` | Sender address for invitation emails |
|
|
| `APP_URL` | `""` | Base URL for RSVP links in emails |
|
|
|
|
## Key files
|
|
|
|
| Area | Path |
|
|
|------|------|
|
|
| Attendee UI | `src/frontend/.../event-modal-sections/AttendeesSection.tsx` |
|
|
| Event form | `src/frontend/.../scheduler/hooks/useEventForm.ts` |
|
|
| CalDAV client | `src/frontend/.../services/dav/CalDavService.ts` |
|
|
| CalDAV proxy | `src/backend/core/api/viewsets_caldav.py` |
|
|
| Scheduling callback | `src/backend/core/api/viewsets_caldav.py` (`CalDAVSchedulingCallbackView`) |
|
|
| RSVP handler | `src/backend/core/api/viewsets_rsvp.py` |
|
|
| Email service | `src/backend/core/services/calendar_invitation_service.py` |
|
|
| ICS parser | `src/backend/core/services/calendar_invitation_service.py` (`ICalendarParser`) |
|
|
| Email templates | `src/backend/core/templates/emails/calendar_invitation*.html` |
|
|
| SabreDAV sanitizer | `src/caldav/src/CalendarSanitizerPlugin.php` |
|
|
| SabreDAV attendee dedup | `src/caldav/src/AttendeeNormalizerPlugin.php` |
|
|
| SabreDAV callback plugin | `src/caldav/src/HttpCallbackIMipPlugin.php` |
|
|
|
|
## Future: Messages mail client integration
|
|
|
|
La Suite includes a Messages mail client (based on an open-source
|
|
webmail). Future integration would allow:
|
|
|
|
- **Inline RSVP** — render Accept/Decline buttons directly in the
|
|
Messages UI when an email contains a `text/calendar` attachment with
|
|
`METHOD:REQUEST`
|
|
- **Calendar preview** — show event details (date, time, location)
|
|
extracted from the ICS attachment without opening the full calendar
|
|
- **Auto-add to calendar** — accepted events automatically appear in
|
|
the user's Calendars calendar via a shared CalDAV backend
|
|
- **Status sync** — PARTSTAT changes in Messages propagate to
|
|
Calendars and vice versa
|
|
|
|
This requires Messages to support iTIP processing
|
|
(`CALENDAR_ITIP_ENABLED=True`) and share the same CalDAV/auth
|
|
infrastructure.
|