Skip to content

Only the organizer may edit or move a meeting - #2929

Open
brhellman wants to merge 8 commits into
Foundry376:masterfrom
brhellman:organizer-only-editing
Open

brhellman wants to merge 8 commits into
Foundry376:masterfrom
brhellman:organizer-only-editing

Conversation

@brhellman

@brhellman brhellman commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

RFC 5546 section 2.1.4: only the organizer revises a meeting. The editor and dragging were gated on the calendar being writable alone, so an attendee could retitle, move or re-rule a meeting somebody else organizes. The write changed nothing but their own copy - every other guest still held the original - and a server implementing scheduling (Google does) is entitled to reject it outright. In practice: an attendee drags a meeting an hour later, sees it move, and is the only person in the world who thinks it did.

  • CalendarEventPopover._isEditable() and canMoveEvent both refuse a meeting this account does not organize (isMine from Answer or counter an invitation from the calendar's right-click menu #2928 - the ORGANIZER is us, or the event names none and is nobody's meeting but ours, or Google rewrote the ORGANIZER to a ...@group.calendar.google.com id on a calendar the server (Record whether each calendar is the account's own or shared into it #2916) says is ours). A new event and a read-only calendar behave as before. The two paths share the test, so what can be dragged and what can be edited never disagree.
  • A guest's drag is not silently ignored: it begins (canAttemptMove - writable calendar, not cancelled), and once it is clearly a drag the grid clears it and shows a dialog - "This meeting can't be rescheduled. Only the organizer can move it." - with Propose a new time and Cancel; the arrow keys get the same dialog. The button opens the counter-proposal picker the context menu uses.
  • The read-only card an attendee lands on offers Propose a new time... in place of the pencil - the one affordance that replaces editing (RFC 5546 section 3.2.7, via Propose a new time for an invitation #2926's popover) - so it is not hidden behind a right-click.

Double-clicking an event we organize to open the editor directly is its own PR, stacked on this one.

Stacks on #2928 (isMine) and through it on the scheduling PRs; carries their commits. @manilabui - this changes the popover's render gate and the read-only card; I've kept it to those hunks. Say if it collides with what you have open.

Verified: 4 canMoveEvent and 2 canAttemptMove cases (theirs refused; ours allowed; ours on read-only / cancelled / a preview refused; a guest's drag allowed to begin), 4 popover cases (theirs not editable; ours editable; read-only never; new event always), 5 data-source cases for isMine (no organizer; somebody else's address; the group-calendar id on a calendar the server says is ours, and not on one it says is shared; ownCalendarIds taking only mine), calendar-guest-move-spec (4: the drag turned into the offer only past the threshold, our own drag left alone, an arrow key, silence on a read-only calendar) and the dialog's two answers - with the popover, drag, context-menu, data-source and calendar-RSVP suites 131 passing. 21 mutations across isMine's terms, both gates, the drag/key interception and the dialog, all red. In a live Google account 68 of 939 events carry a group-calendar ORGANIZER; all sit on a calendar the server reports as somebody else's, so they stay not ours there. Lint, prettier and tsc clean. Extracted from the calendar-scheduling branch (the release-notes-worthy behaviour change there since 2026-08-28: attendees can no longer edit meetings they don't organize).

🤖 Generated with Claude Code

@manilabui

Copy link
Copy Markdown
Contributor

This works as described on my calendar. I'm fine with guests not moving meetings they don't organize, but right now the drag just does nothing. Can a guest who tries to move one get a dialog saying the event can't be rescheduled, with a "Propose a new time" button on it?

38 of my events have a …@group.calendar.google.com organizer. If Google rewrites ORGANIZER on a secondary calendar as #2924 says, are any of those my own meetings, and can isMine take calendar ownership into account?

recurrenceLoaded isn't protected (removing it from the save condition, defaulting it to true, or deleting the load on mount all leave 145 green), and the _loadEditDefaults comment says the form isn't shown until the load has run, but startEditing shows it straight away. Could the double-click-opens-the-editor change come as its own PR?

A possible follow-up, not for this PR: telling apart guests the organizer has allowed to modify the event, and offering them a way to edit it in Google Calendar. The synced copy doesn't carry that permission, so it would need a sync engine update as well.

🤖 Generated with Claude Code

RFC 5546 section 2.1.4: the organizer increments SEQUENCE when they change
something that matters to the guests, and a guest's client ignores an update
whose SEQUENCE has not advanced. Five helpers each bumped it when the property
happened to be present, so one save that changed time, guests and recurrence
advanced three times, and a save on an event with no SEQUENCE never advanced
at all. The helpers now leave SEQUENCE alone; the caller that assembles a
revision calls bumpEventSequence once, on the master or on the edited
occurrence's exception, and an absent SEQUENCE counts as zero (RFC 5545
section 3.7.4).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@brhellman
brhellman force-pushed the organizer-only-editing branch from f05ddbd to adb324d Compare October 2, 2026 21:06
@brhellman

Copy link
Copy Markdown
Contributor Author

Rebased onto #2928's new head as adb324d7d.

  1. A guest's drag now begins (canAttemptMove: writable calendar, not cancelled, not a preview) and, once it is past the drag threshold, the grid clears it and shows a dialog - "This meeting can't be rescheduled / Only the organizer can move it" - with Propose a new time and Cancel; the arrow keys get the same dialog. offerCounterInsteadOfMove in calendar-rsvp.ts opens the same picker the context menu uses. Specs: calendar-guest-move-spec.ts (drag past the threshold, drag of our own meeting left alone, arrow key, read-only stays silent) and the dialog's two answers in calendar-rsvp-spec.ts.
  2. Group-calendar organizers: isMine is now also true when the ORGANIZER is a ...@group.calendar.google.com id and the event sits on a calendar the server (DAV:owner, Record whether each calendar is the account's own or shared into it #2916) says is ours; the data source reads the calendars for that. In Brian's live DB the 68 such events (of 939) are all on "LINBIT Bereitschaft", which the server reports as someone else's, and none list him as an attendee, so they stay not ours there; on a secondary calendar you own they become editable. Four cases in calendar-data-source-spec.ts.
  3. recurrenceLoaded is gone: the save already writes the rule only when the Repeat control differs from what was read, so the flag protected nothing a spec could reach. The comment went with it.
  4. Double-click opening the editor is its own PR now, stacked on this one, with the startEditing prop and the on-mount read of the rule (spec'd: the rule is read off the event, and not for a new event).

Mutations run: 19 across isMine's three terms, canAttemptMove/canMoveEvent, _isEditable, the drag and arrow-key interception and the dialog's answer; all red.

Brian Hellman and others added 7 commits October 2, 2026 21:24
…hen there is none

Answering an invitation from the message did one of the two things an RSVP
is: it emailed the organizer a REPLY. Under CalDAV the attendee also writes
their PARTSTAT back to their own copy of the event (RFC 6638 section 3.2.5),
and for a Google guest that write is what registers: against a real Google
organizer the emailed REPLY was delivered and ignored, while the PARTSTAT
written over CalDAV showed on the organizer's calendar within a minute. So
the calendar kept showing the meeting as unanswered - or, for an invitation
Google had not put on the calendar at all, showed nothing.

resolveRSVPTarget picks the copy we are entitled to write to: one UID can sit
on our calendar, a room's and a colleague's at once, and the header now says
which calendar the answer goes to, or why it will only be emailed. When the
event is on none of our calendars, accepting creates it on our own (never for
an invitation naming us as ORGANIZER, which a scheduling server would turn
into outbound mail). The copy's VEVENT shown and answered is the one
eventForInvitation picks for the invitation, and the REPLY is addressed to
the organizer the mailed invitation names, because Google rewrites ORGANIZER
on a shared calendar's copy to an address nobody reads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Answering from the message meant opening the calendar to check the slot.
ICSEventHelpers.findConflicts expands the account's stored events across the
slot being answered - the next occurrence of a recurring invitation, else the
event itself - honouring TRANSP:TRANSPARENT, CANCELLED and this account's own
DECLINED, leaving out all-day events, read-only subscribed feeds, calendars
hidden in the sidebar and calendars the server says are someone else's. The
header lists what overlaps, the way Google Calendar does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Only the organizer may revise a meeting (RFC 5546 section 2.1.4), so an
attendee who cannot make the slot could accept, decline, or reply by hand.
iTIP's COUNTER (section 3.2.7) is the third answer - Google Calendar's
"Propose a new time" - and the sync engine has accepted it since
Mailspring-Sync#130. This adds the pieces the calendar's context menu (Foundry376#2928)
wires up: ProposeTimePopover picks the slot (dates only for an all-day
invitation) and an optional note, createCounterProposal builds the COUNTER
from the invitation with only the proposer listed and one occurrence named
by RECURRENCE-ID, and EventRSVPTask sends it without marking the invitation
answered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	app/spec/ics-event-helpers-spec.ts
A COUNTER that arrives by email asks us, as organizer, to move the meeting.
The header shows the proposed slot and offers to move our copy to it - but
only when our own synced copy says this account organizes the meeting (RFC
5546 section 2.1.4) and the message's sender is on its guest list (section
3.2.7). Nothing in the attachment is trusted for that: its UID, ORGANIZER and
ATTENDEE list are the sender's, and a UID is not a secret. When either check
fails the proposal still renders, with a line saying why it cannot be applied.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An invitation that this client's own accept flow put on the calendar has no
email to hand by the time someone wants to change their answer, and an
attendee may not edit the meeting itself (RFC 5546 section 2.1.4). The event's
context menu now carries Accept / Maybe / Decline - which both email the
organizer a REPLY and write our PARTSTAT onto our copy, refusing a copy the
server says is somebody else's - and Propose New Time, which sends a COUNTER
naming the occurrence that was right-clicked (dates only for an all-day one)
and writes nothing locally, so it is offered on a read-only calendar too.
Occurrences learn isMine (this account organizes the event, or nobody does)
so the menu can say Edit or View, and the organizer address is normalised
once in the data source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RFC 5546 section 2.1.4: only the organizer revises a meeting. An attendee who
edited or dragged one here changed nothing but their own copy - every other
guest still held the original - and a scheduling server is entitled to reject
the write. The editor and dragging now refuse a meeting this account does not
organize (isMine): the card an attendee lands on offers Propose a new time in
place of the pencil, and a drag or arrow key on such a meeting opens a dialog
saying only the organizer can move it, with the counter-proposal on offer.
isMine also counts an event whose ORGANIZER Google rewrote to a secondary
calendar's own @group.calendar.google.com id, when the server says that
calendar is ours.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@brhellman

Copy link
Copy Markdown
Contributor Author

The double-click-opens-the-editor change is now its own PR, #2937, stacked on this one.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants