[Scheduling] Emailed iCalendar invites on session confirm / reschedule / cancel #189

Closed
opened 2026-07-17 21:36:53 +00:00 by claude-bot · 0 comments
Contributor

Context

Replaces the dropped Google Calendar OAuth push (#107 Phase B — see that PR's investigation: sensitive-scope verification + a non-resettable 100-user cap make per-self-hoster Google OAuth poor value). The #107 iCal feed gives subscription sync, but Google refreshes external ICS feeds only every 8–24h.

Emailed iCalendar invites are the near-instant, provider-agnostic, no-OAuth alternative: an invite email auto-adds the session to Gmail / Outlook / Apple Calendar, updates in place on reschedule, and is removed on cancel — via the standard iTIP METHOD:REQUEST / METHOD:CANCEL mechanism.

Current state to be aware of: today's send_confirmation (app/notifications/email.py) sends a plain HTML email to a single configured SMTP address — there is no per-member email path and no calendar part. So this is a real feature, not just "attach an .ics".

Spec

Per-member calendar invites (app/notifications/email.py + a new/extended service):

  • On session confirm: for each campaign member who has an email and has opted in, send an email whose body is multipart/mixed with the HTML part and a text/calendar; method=REQUEST part (plus a .ics attachment for clients that prefer it), built via the existing app/services/calendar_service.py from #107.
    • UID = {session.id}@questboard (stable — matches the feed, so a client that has both won't duplicate).
    • SEQUENCE = int(session.updated_at.timestamp()) (reuse #107's updated_at).
    • ORGANIZER = the configured from-address; ATTENDEE = the member's email; DTSTART/DTEND UTC.
  • On reschedule: re-send METHOD:REQUEST with the (higher) SEQUENCE → clients update in place.
  • On cancel: send METHOD:CANCEL with a higher SEQUENCE → clients remove the event.

Opt-in:

  • Add users.calendar_invite_opt_in (boolean, default false), mirroring recap_email_opt_in. Editable on Profile. Only opted-in members with a non-null email receive invites. No-op entirely if SMTP is unconfigured.

Wiring: hook into the existing send_session_confirmed / reschedule / send_session_cancelled task paths. Calendar invites are an additional channel alongside Discord/webhook notifications, not a replacement.

Out of scope

  • Inbound RSVP / METHOD:REPLY handling (attendee responses back into Quest Board).
  • The subscribable feed (done in #107) and Google OAuth (dropped).

Acceptance criteria

  • A confirmed session emails each opted-in member an invite that adds to Gmail/Apple/Outlook (verify METHOD:REQUEST, correct UID/ATTENDEE/DTSTART).
  • Rescheduling re-sends with a higher SEQUENCE and updates the existing event (same UID).
  • Cancelling sends METHOD:CANCEL and removes it.
  • Members who haven't opted in, or have no email, are not emailed; SMTP-unconfigured is a silent no-op.
  • Migration for calendar_invite_opt_in applies and round-trips.

References

  • app/notifications/email.py (send_confirmation / _send), app/tasks/reminder_tasks.py (send_session_confirmed, send_session_cancelled, reschedule path)
  • app/services/calendar_service.py (#107 ICS builder — extend with METHOD + ORGANIZER/ATTENDEE)
  • app/models/user.py (email, recap_email_opt_in as the opt-in precedent)

Origin: replacement for #107 Phase B, chosen 2026-07-17.

## Context Replaces the **dropped Google Calendar OAuth push** (#107 Phase B — see that PR's investigation: sensitive-scope verification + a non-resettable 100-user cap make per-self-hoster Google OAuth poor value). The #107 iCal *feed* gives subscription sync, but Google refreshes external ICS feeds only every **8–24h**. Emailed iCalendar invites are the near-instant, provider-agnostic, **no-OAuth** alternative: an invite email auto-adds the session to Gmail / Outlook / Apple Calendar, updates in place on reschedule, and is removed on cancel — via the standard iTIP `METHOD:REQUEST` / `METHOD:CANCEL` mechanism. **Current state to be aware of:** today's `send_confirmation` (`app/notifications/email.py`) sends a **plain HTML** email to a **single configured SMTP address** — there is *no* per-member email path and *no* calendar part. So this is a real feature, not just "attach an .ics". ## Spec **Per-member calendar invites (`app/notifications/email.py` + a new/extended service):** - On session **confirm**: for each campaign member who has an email **and** has opted in, send an email whose body is `multipart/mixed` with the HTML part **and** a `text/calendar; method=REQUEST` part (plus a `.ics` attachment for clients that prefer it), built via the existing `app/services/calendar_service.py` from #107. - `UID` = `{session.id}@questboard` (stable — matches the feed, so a client that has both won't duplicate). - `SEQUENCE` = `int(session.updated_at.timestamp())` (reuse #107's `updated_at`). - `ORGANIZER` = the configured from-address; `ATTENDEE` = the member's email; `DTSTART`/`DTEND` UTC. - On **reschedule**: re-send `METHOD:REQUEST` with the (higher) `SEQUENCE` → clients update in place. - On **cancel**: send `METHOD:CANCEL` with a higher `SEQUENCE` → clients remove the event. **Opt-in:** - Add `users.calendar_invite_opt_in` (boolean, default false), mirroring `recap_email_opt_in`. Editable on Profile. Only opted-in members with a non-null `email` receive invites. No-op entirely if SMTP is unconfigured. **Wiring:** hook into the existing `send_session_confirmed` / reschedule / `send_session_cancelled` task paths. Calendar invites are an **additional** channel alongside Discord/webhook notifications, not a replacement. ## Out of scope - Inbound RSVP / `METHOD:REPLY` handling (attendee responses back into Quest Board). - The subscribable feed (done in #107) and Google OAuth (dropped). ## Acceptance criteria - A confirmed session emails each opted-in member an invite that adds to Gmail/Apple/Outlook (verify `METHOD:REQUEST`, correct `UID`/`ATTENDEE`/`DTSTART`). - Rescheduling re-sends with a higher `SEQUENCE` and updates the existing event (same `UID`). - Cancelling sends `METHOD:CANCEL` and removes it. - Members who haven't opted in, or have no email, are not emailed; SMTP-unconfigured is a silent no-op. - Migration for `calendar_invite_opt_in` applies and round-trips. ## References - `app/notifications/email.py` (`send_confirmation` / `_send`), `app/tasks/reminder_tasks.py` (`send_session_confirmed`, `send_session_cancelled`, reschedule path) - `app/services/calendar_service.py` (#107 ICS builder — extend with `METHOD` + `ORGANIZER`/`ATTENDEE`) - `app/models/user.py` (`email`, `recap_email_opt_in` as the opt-in precedent) _Origin: replacement for #107 Phase B, chosen 2026-07-17._
rbrooks referenced this issue from a commit 2026-07-17 22:29:39 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#189
No description provided.