Skip to content

fix(calendar): send reservation emails as iTip REQUEST and CANCEL - #1790

Open
lucs7 wants to merge 1 commit into
LibreBooking:developfrom
lucs7:feature/ics-rework/invitees
Open

lucs7 wants to merge 1 commit into
LibreBooking:developfrom
lucs7:feature/ics-rework/invitees

Conversation

@lucs7

@lucs7 lucs7 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Reservation emails always attached an ICS with METHOD:REQUEST, so
Outlook offered to add the event again on cancel. Clients disagree on
what they accept, and a REQUEST needs an ATTENDEE (RFC 5546).

Changes

  • Add IcsMethod. Deletion emails send CANCEL, all other emails send
    REQUEST. Feeds and exports keep PUBLISH.
  • Organizer is the default from address, the only attendee is the
    recipient (owner, invitee, guest or share address), with
    PARTSTAT=NEEDS-ACTION and RSVP=FALSE.
  • A series without BookedBy() falls back to the owner's session
    instead of failing.

Testing

  • Data-driven test over all 13 email types (method, mime type,
    attendee, status on cancel).
  • Privacy matrix for every method.
  • Checked by hand with the dev mail server: create and delete in Gmail
    and Thunderbird. Not retested in Outlook with the final layout.

Closes: #1701

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 9, 2026 21:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lucs7
lucs7 marked this pull request as draft October 10, 2026 14:05
@lucs7
lucs7 force-pushed the feature/ics-rework/invitees branch 3 times, most recently from 9b11091 to 0c31259 Compare October 10, 2026 21:54
@lucs7 lucs7 changed the title fix(calendar): send the correct iTip METHOD per email type fix(calendar): send reservation emails as iTip REQUEST and CANCEL Oct 10, 2026
@lucs7
lucs7 marked this pull request as ready for review October 10, 2026 22:01
@lucs7
lucs7 requested a balanced review from Copilot October 10, 2026 22:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@lucs7
lucs7 force-pushed the feature/ics-rework/invitees branch from 0c31259 to a7f21b1 Compare October 11, 2026 09:54
Every reservation email attached an ICS with METHOD:REQUEST, so
Outlook offered to add the event again when a reservation was
cancelled. Clients also differ on what they accept: Outlook only
recognises REQUEST, and a REQUEST needs an ATTENDEE to follow RFC 5546.

Add an IcsMethod enum and let each email class pick its method:
CANCEL (STATUS:CANCELLED) for deletion emails and REQUEST for
everything else. Feeds and exports keep PUBLISH.

REQUEST and CANCEL name the site's default from address as the
organizer and whoever receives the email as the only attendee: the
owner, an invitee, a guest or a share recipient. The attendee needs
action and no RSVP is requested, because invitations are answered
through the links in the email.

The ICS no longer carries a SEQUENCE. Thunderbird did not match a
cancellation to its event when SEQUENCE was set, and a missing one
counts as 0, so DTSTAMP orders the messages.

A series without BookedBy() (from BuildSeries()) now falls back to the
owner's session, with a timezone, instead of failing in the summary
formatting. The log for a missing default from address is kept.

The owner's update email used the subject meant for administrators
("Notification: Reservation Updated for ..."). Use the one for the
owner, like the created and deleted emails do.

Assisted-by: Claude:claude-sonnet-5-5

@JohnVillalovos JohnVillalovos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was written by Codex (OpenAI’s AI coding assistant).

[P1] Preserve an incrementing SEQUENCE for cancellation messages

Pages/Export/CalendarExportDisplay.php, lines 70–73

Switching deletion emails to METHOD:CANCEL is appropriate, but removing SEQUENCE makes those messages noncompliant with RFC 5546 §3.2.5, which requires a sequence number and requires it to increase when cancelling an event. A later DTSTAMP does not replace that requirement. Clients enforcing it may reject the cancellation and leave the reservation on the recipient’s calendar.

Please emit a monotonically increasing revision for each event, including cancellations. Replace the assertion requiring SEQUENCE to be absent with coverage for a create → update → cancel lifecycle. The reported Thunderbird behavior deserves investigation, but dropping a required property is not a portable fix.

The commit message and PR description also say feeds and exports “keep PUBLISH,” although the previous implementation emitted REQUEST; please describe this as a change.

Validation against d13c5f72a: all 1,884 PHPUnit tests completed without failures, with warnings/deprecations and skipped/incomplete tests. PHPStan reported zero errors. I did not directly test calendar clients.

@JohnVillalovos JohnVillalovos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was written by Claude Code (Anthropic's AI coding assistant).

Thanks for this. Sending CANCEL for deletions and naming the recipient as ATTENDEE is the right direction, and the data-driven test over every email type is a good addition. I agree with the earlier review about SEQUENCE and the "keep PUBLISH" wording. This review covers the remaining points and doesn't repeat those.

Checked against d13c5f72a: the full PHPUnit suite passes (1884 tests), PHPStan reports 0 errors, and php-cs-fixer is clean. I didn't test any calendar clients.

Must fix

1. Split the unrelated fixes into their own commits. Two independent bug fixes ride along with the iTip change:

  • the BookedBy() null fallback in ReservationEmailMessage::PopulateIcsAttachment(). This is a real crash fix for series loaded through BuildSeries(), such as the approval email.
  • the ReservationUpdatedEmail subject change, from the admin subject to the owner subject.

Both are good changes. As separate fix: commits they can be reviewed, bisected and reverted on their own.

2. Document every behaviour change in the commit message and PR description. Besides the PUBLISH point from the earlier review, these changes aren't mentioned:

  • Feeds and .ics downloads now put SENT-BY="mailto:<default from>" on ORGANIZER.
  • The Outlook workaround from a886d25b4 is removed. When the viewer owned the reservation, ORGANIZER used to be rewritten to user-noreply@domain. Now feeds and downloads use the owner's real address. With METHOD:PUBLISH this is probably fine, but the PR says Outlook wasn't retested. Please check that importing a downloaded .ics as the reservation owner still works in Outlook.

The sentence "Outlook only recognises REQUEST" is also misleading, because this PR relies on Outlook handling CANCEL. I think the point is that Outlook doesn't treat a PUBLISH attachment as an invitation. Please reword it.

Should fix

3. Pages/Export/CalendarExportDisplay.php:60 and :103: !empty($res->OrganizerSentBy) is used as a hidden flag meaning "the owner is visible and a default from address is set". An explicit property, or passing the attendee into Render(), would make the organizer/attendee decision easier to follow.

4. Pages/Export/CalendarExportDisplay.php:102: emails always set AttendeeEmail, so only the tests reach the $res->AttendeeEmail ?? $res->OrganizerEmail fallback. Making the owner the attendee by default would hide a caller that forgot to set one. Please remove the fallback, or document when it applies.

5. Empty email.default.from.address: a REQUEST then goes out with the owner as ORGANIZER and no ATTENDEE. That breaks the RFC 5546 rule this PR cites, and it's the "you are the organizer" situation the removed workaround guarded against. The setting has a default value, so this is rare. Still, the "missing default from address" log only fires in the privacy-hidden branch, not this one.

6. ReservationEmailMessage.php: $display->Render([$icsView], null, $method) passes a positional null. Project guidelines ask for named arguments: Render(reservations: [$icsView], method: $method).

7. tests/lib/Email/Messages/ReservationEmailMessageTest.php:71: emailTypes() doesn't list ParticipantDeletedEmail. That's the most common CANCEL path, because every participant gets one when a reservation is deleted. It inherits CANCEL through InviteeRemovedEmail, so the test would pass. Please add it so a change to that inheritance is caught.

Nits

  • The tests call the protected method two ways: through (fn () => $this->PopulateIcsAttachment(...))->call($message), and through the existing TestReservationEmailMessage::PopulateIcsAttachmentForTest(). Please pick one.
  • The foreach over require_once at the top of ReservationEmailMessageTest.php is unusual for this codebase.
  • CalendarExportPresenterTest uses the fully qualified #[\PHPUnit\Framework\Attributes\DataProvider], while ReservationEmailMessageTest imports it with use.
  • PARTSTAT=NEEDS-ACTION for participants who have already accepted, and ROLE=REQ-PARTICIPANT for share recipients, aren't accurate. ACCEPTED and NON-PARTICIPANT would describe them better.
  • With ORGANIZER set to the site address, invitees no longer see who booked the reservation in their calendar entry. This is a reasonable trade-off, but please mention it in the description.

Follow-up issues (not blockers, they predate this PR)

  • Invitees and share recipients get a REQUEST but never a CANCEL when the whole reservation is deleted. DeleteReservationNotificationService doesn't notify them, so their calendar entries stay behind.
  • Declining or leaving through the LibreBooking links sends no CANCEL, so the event stays in that user's calendar.
  • Creating or deleting a recurring series only sends an ICS for the current instance.
  • Even with RSVP=FALSE, Outlook and Gmail show Accept/Decline buttons. A reply goes to the no-reply mailbox and LibreBooking never sees it. The email template could tell people to use the links instead.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolve the missing attendee condition and restore the required cancellation sequence number.

2 open issues

Review effort: Lite

$event->add('ORGANIZER', 'mailto:' . $res->OrganizerEmail, $organizerParams);
}
$event->add('STATUS', $method === IcsMethod::CANCEL ? 'CANCELLED' : ($res->IsPending ? 'TENTATIVE' : 'CONFIRMED'));
$event->add('SUMMARY', $res->Summary);
}

$attendeeEmail = $res->AttendeeEmail ?? $res->OrganizerEmail;
if ($method !== IcsMethod::PUBLISH && !empty($res->OrganizerSentBy) && !empty($attendeeEmail)) {

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.

[Enhancement] Outlook 365 link in reservation email

3 participants