Commit Graph
2 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 9bf7d3797d Take in invitations that arrive by mail
A watched mailbox is a trust boundary this app did not have: anyone who knows
the address can hand the server an iCalendar file. So a mail can do exactly one
thing — create a row in event_invites. No calendar read path touches that
table, and an invitation becomes an event only when a person accepts it.

Routing is by the ATTENDEE address, never the From header, which a sender can
claim freely. Addresses are matched against users.email plus a dedicated table,
so forwarded mail still finds its owner: the invitation names the original
address, not the mailbox it landed in.

An accepted invitation gets a fresh UID; the organiser's goes to external_uid.
LocalEvent.uid is globally unique, so reusing it would let a stranger's mail
collide with an event already there. That case has a test, because it is the
one that would be a real problem.

parse_ics now reads METHOD, SEQUENCE, the organiser address, the ATTENDEE list
and RECURRENCE-ID. All additive — existing callers read the keys they knew.
METHOD sits on the VCALENDAR, so it is read outside the component loop; that is
the only reliable way to tell an invitation from a plain calendar file. A VEVENT
carrying RECURRENCE-ID is skipped rather than applied, since treating one
occurrence as the series would move every other date.

The mailbox job registers only when IMAP_HOST is set: an unconditional job would
log every few minutes that it has nothing to do, and test_scheduler asserts the
exact job set. Credentials come from the environment, which keeps the one secret
that opens a whole mailbox out of a table an endpoint could return.

Replies to the organiser are the next step; accepting and declining work without
them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-29 17:16:25 +01:00
ScarriffleandClaude Opus 5 4b9c09df74 feat(scheduler): Hintergrund-Jobs im Serverprozess
Das Backend hatte bisher keinerlei Hintergrund-Mechanik. Neues Modul mit
einer Job-Registry, angebunden ueber den FastAPI-Lifespan. Der Server
laeuft als genau ein Prozess, ein In-Process-Loop laeuft damit garantiert
nur einmal -- steht als Vorbehalt im Modulkopf, falls spaeter Worker
dazukommen.

Jeder Job laeuft im Thread mit eigener Session, weil SQLAlchemy hier
synchron ist und Datei- oder Netz-I/O sonst den Event-Loop blockiert.
Fehler werden pro Job gefangen.

Zwei Jobs: der Anhang-Sweep alle 24 h und die iCal-Aktualisierung alle
15 min. Letztere treibt nur den vorhandenen _refresh_if_needed an, der das
Intervall je Abo schon respektiert -- neu ist allein, dass Abos auch
aktuell bleiben, wenn niemand die App offen hat. Jedes Abo einzeln
gekapselt, weil der Helfer ohne Cache weiterwirft.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:14:28 +02:00