fix(caldav): write ORGANIZER so an event with attendees invites somebody #40

Merged
jlxq0 merged 1 commit from fix-organizer into main 2026-08-31 02:00:09 +00:00
Owner

Closes #39. Not merged, per your instruction.

The fix

ORGANIZER set to the authenticated principal, taken from the identity the tool layer already holds. No new parameter, no caller change.

Written only when there are attendees. Writing it on a solo event would make everything this tool creates a scheduling object in Stalwart's eyes, which changes what later edits do rather than fixing what this one does. That is a narrowing of the instruction and I would rather flag it than bury it.

Attendees with no organizer available are refused. A token carrying no email claim would otherwise recreate the original defect silently, and a refusal that names the reason is the only outcome that does not.

The control, which is the part worth reading

Before adding any test, I removed the new ORGANIZER line and ran the pre-existing suite:

124 passed; 0 failed

Green with the bug and green without it. That is what let this ship, and it is why the acceptance had to be the stored object.

Mutations, each with the pattern asserted and the harness checked

mutation died
ORGANIZER not written an_event_with_attendees_carries_an_organizer_in_the_stored_object, create_event_puts_an_organizer_when_there_are_attendees
written even with no attendees an_event_without_attendees_has_no_organizer, plus two pre-existing attendee-free tests
ORGANIZER dropped on read an_event_with_attendees_carries_an_organizer_in_the_stored_object

The third matters because an organizer the parser drops is the same defect one layer over: the stored object would be right and every read would say organizer: null.

The wiremock test asserts the PUT body, and mounts a mock that only matches a body containing both ORGANIZER and ATTENDEE, so a write missing either fails verification rather than passing quietly.

Your two questions, answered here rather than deferred

Does updating an event with attendees issue a REQUEST? For an event created after this fix, yes. patch_ics edits the master in place and passes ORGANIZER through, so the object stays a scheduling object and the server's organizer diff fires. Pinned by an_update_carries_the_organizer_through.

Does removing an attendee issue a CANCEL? Same mechanism and the same answer, from Stalwart's organizer-side diff: an attendee present in the old object and absent in the new produces CANCEL. It needs the object to carry ORGANIZER, which it now does.

And the half neither question asked. Events created before this fix have no ORGANIZER, so updating or deleting them still notifies nobody. I have not backfilled one on update, deliberately: doing so would start sending mail about events that have never notified anyone, which is the irrecoverable direction and exactly what delete_occurrence's refusal exists to prevent. The test pins that an update does not add one.

Read path

Already surfaced. organizer is parsed from ORGANIZER and returned on every event; organizer: null was accurate rather than a display bug.

Gates

Toolchain read out of .forgejo/workflows/ci.yml (toolchain: stable, rustc 1.98.0 locally, current stable). fmt --check, clippy -D warnings, test --all-features (129 + 9), audit, deny — all green, all through build-slot.sh.

Closes #39. **Not merged**, per your instruction. ## The fix `ORGANIZER` set to the authenticated principal, taken from the identity the tool layer already holds. No new parameter, no caller change. **Written only when there are attendees.** Writing it on a solo event would make everything this tool creates a scheduling object in Stalwart's eyes, which changes what later edits do rather than fixing what this one does. That is a narrowing of the instruction and I would rather flag it than bury it. **Attendees with no organizer available are refused.** A token carrying no email claim would otherwise recreate the original defect silently, and a refusal that names the reason is the only outcome that does not. ## The control, which is the part worth reading Before adding any test, I removed the new `ORGANIZER` line and ran the pre-existing suite: ```text 124 passed; 0 failed ``` **Green with the bug and green without it.** That is what let this ship, and it is why the acceptance had to be the stored object. ## Mutations, each with the pattern asserted and the harness checked | mutation | died | |---|---| | `ORGANIZER` not written | `an_event_with_attendees_carries_an_organizer_in_the_stored_object`, `create_event_puts_an_organizer_when_there_are_attendees` | | written even with no attendees | `an_event_without_attendees_has_no_organizer`, plus two pre-existing attendee-free tests | | `ORGANIZER` dropped on read | `an_event_with_attendees_carries_an_organizer_in_the_stored_object` | The third matters because an organizer the parser drops is the same defect one layer over: the stored object would be right and every read would say `organizer: null`. The wiremock test asserts the **`PUT` body**, and mounts a mock that only matches a body containing both `ORGANIZER` and `ATTENDEE`, so a write missing either fails verification rather than passing quietly. ## Your two questions, answered here rather than deferred **Does updating an event with attendees issue a `REQUEST`?** For an event created after this fix, yes. `patch_ics` edits the master in place and passes `ORGANIZER` through, so the object stays a scheduling object and the server's organizer diff fires. Pinned by `an_update_carries_the_organizer_through`. **Does removing an attendee issue a `CANCEL`?** Same mechanism and the same answer, from Stalwart's organizer-side diff: an attendee present in the old object and absent in the new produces `CANCEL`. It needs the object to carry `ORGANIZER`, which it now does. **And the half neither question asked.** Events created *before* this fix have no `ORGANIZER`, so updating or deleting them still notifies nobody. **I have not backfilled one on update, deliberately**: doing so would start sending mail about events that have never notified anyone, which is the irrecoverable direction and exactly what `delete_occurrence`'s refusal exists to prevent. The test pins that an update does not add one. ## Read path Already surfaced. `organizer` is parsed from `ORGANIZER` and returned on every event; `organizer: null` was accurate rather than a display bug. ## Gates Toolchain read out of `.forgejo/workflows/ci.yml` (`toolchain: stable`, `rustc 1.98.0` locally, current stable). `fmt --check`, `clippy -D warnings`, `test --all-features` (129 + 9), `audit`, `deny` — all green, all through `build-slot.sh`.
fix(caldav): write ORGANIZER so an event with attendees invites somebody
All checks were successful
CI / cargo (pull_request) Successful in 46s
CI / docker (pull_request) Successful in 57s
67b63b9ec9
create_event wrote ATTENDEE lines and no ORGANIZER. RFC 6638 scheduling keys on
ORGANIZER, so the object was not a scheduling object, Stalwart correctly emitted
no iTIP, and every event created with attendees invited nobody.

Every signal afterwards said it worked: the event is on the calendar,
list_events returns the attendee, and the create response echoes attendees
beside organizer: null. Nothing distinguished an event that invited somebody
from one that invited nobody.

The organizer is the authenticated principal, taken from the identity the tool
layer already holds, so no caller change and no new parameter. It is written
only when there are attendees: writing it on a solo event would make everything
this tool creates a scheduling object, which changes what later edits do rather
than fixing what this one does. Attendees with no organizer available are
refused rather than written, because a token without an email claim would
otherwise recreate the defect silently.

The tests assert the stored object rather than the response. Asserting on the
response passes against the broken code, since the response is built from the
same struct that omitted the field. Verified by removing the ORGANIZER line
and watching the pre-existing suite stay green at 124 passed.

Also answers the two questions on the issue rather than leaving them: an update
carries ORGANIZER through and does not add one, so an event created after this
fix notifies on edit and on attendee removal, and one created before it still
notifies nobody. Backfilling on update would start sending mail about events
that have never notified anyone.

Closes #39
Author
Owner

Merging. 129 + 9 passed here, and I ran the call-site mutation nobody had.

src/mcp.rs:571
organizer: identity.as_ref().and_then(|v| v.email.clone())  ->  organizer: None
129 passed; 0 failed        9 passed; 0 failed

Nothing asserts that the handler supplies the principal's email, so the whole fix can be disconnected from its only caller and the suite says nothing. Sixth instance of that class this week and the first in this repository.

Merging anyway, and the reason is what it degrades to. With the organizer cut, the refusal branch fires and every event with attendees is refused with a clear message. That is loud rather than silent, so the untested wiring cannot reproduce the defect this issue is about. Same column as matrix-mcp#144, opposite of m365-mcp#28.

File it with this mutation in it, and say that it degrades to a refusal rather than to the original fault, so nobody reads it as more urgent than it is.

What I checked rather than took

Your pre-emptive mutation is the measurement that justifies the whole shape. 124 passed with the bug and 124 passed without it, run before you wrote a test, which is why the acceptance had to be the stored object.

The read-side mutation is the one I would not have asked for. An ORGANIZER the parser drops leaves the stored object correct and every read saying organizer: null, which looks exactly like the bug still being there. Catching the defect one layer over, in the change that fixes it, is the thing this week keeps rewarding.

A mock that only matches a body containing both ORGANIZER and ATTENDEE makes a write missing either fail verification rather than pass quietly. An absent mock as the assertion.

The narrowing and the deferral, both right

ORGANIZER only when there are attendees, because writing it unconditionally makes every event a scheduling object and changes what later edits do. Attendees with no organizer refused rather than written, because a token with no email claim would otherwise recreate this defect silently. Flagged rather than buried, which is why it reads as a decision.

Not backfilling ORGANIZER on update of a pre-fix event is the half neither question asked, and the reasoning is the one that decides it: backfilling starts sending mail about events that have never notified anyone, which is the irrecoverable direction. Pinned by a test rather than left as an intention.

**Merging. 129 + 9 passed here, and I ran the call-site mutation nobody had.** src/mcp.rs:571 organizer: identity.as_ref().and_then(|v| v.email.clone()) -> organizer: None 129 passed; 0 failed 9 passed; 0 failed **Nothing asserts that the handler supplies the principal's email**, so the whole fix can be disconnected from its only caller and the suite says nothing. Sixth instance of that class this week and the first in this repository. **Merging anyway, and the reason is what it degrades to.** With the organizer cut, the refusal branch fires and every event with attendees is **refused with a clear message**. That is loud rather than silent, so the untested wiring cannot reproduce the defect this issue is about. Same column as `matrix-mcp#144`, opposite of `m365-mcp#28`. **File it with this mutation in it**, and say that it degrades to a refusal rather than to the original fault, so nobody reads it as more urgent than it is. ## What I checked rather than took **Your pre-emptive mutation is the measurement that justifies the whole shape.** 124 passed with the bug and 124 passed without it, run before you wrote a test, which is why the acceptance had to be the stored object. **The read-side mutation is the one I would not have asked for.** An `ORGANIZER` the parser drops leaves the stored object correct and every read saying `organizer: null`, which looks exactly like the bug still being there. Catching the defect one layer over, in the change that fixes it, is the thing this week keeps rewarding. **A mock that only matches a body containing both `ORGANIZER` and `ATTENDEE`** makes a write missing either fail verification rather than pass quietly. An absent mock as the assertion. ## The narrowing and the deferral, both right **`ORGANIZER` only when there are attendees**, because writing it unconditionally makes every event a scheduling object and changes what later edits do. **Attendees with no organizer refused rather than written**, because a token with no email claim would otherwise recreate this defect silently. Flagged rather than buried, which is why it reads as a decision. **Not backfilling `ORGANIZER` on update of a pre-fix event** is the half neither question asked, and the reasoning is the one that decides it: backfilling starts sending mail about events that have never notified anyone, which is the irrecoverable direction. **Pinned by a test rather than left as an intention.**
jlxq0 merged commit c196b32ba9 into main 2026-08-31 02:00:09 +00:00
jlxq0 deleted branch fix-organizer 2026-08-31 02:00:10 +00:00
Sign in to join this conversation.
No reviewers
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
jlxq0/caldav-mcp!40
No description provided.