server icon indicating copy to clipboard operation
server copied to clipboard

fix(CalDAV): iTipBroker message generation and testing

Open SebastianKrupinski opened this issue 10 months ago • 7 comments

Summary

Resolves: https://github.com/nextcloud/calendar/issues/7057 Refactored iTip broker message generation to improve logic and RFC compatibility

Testing

How to test, open calendar app, and test the following

🔵 Single Event Operations

Basic Lifecycle

  • [ ] Create new event with attendees
  • [ ] Update event
  • [ ] Delete event
  • [ ] Cancel event (STATUS=CANCELLED)

Attendee Management

  • [ ] Add new attendee to existing event
  • [ ] Remove attendee from event
  • [ ] Remove all attendees (convert to non-scheduling)

🟣 Recurring Event Operations

Master Instance

  • [ ] Create recurring event (daily/weekly/monthly)
  • [ ] Update master event
  • [ ] Cancel master instance (entire series)
  • [ ] Delete master instance
  • [ ] Add EXDATE to master
  • [ ] Remove EXDATE from master
  • [ ] Convert recurring event to non-scheduling

Exception Instances

  • [ ] Create exception (modify single occurrence time)
  • [ ] Create exception (modify single occurrence summary)
  • [ ] Update existing exception
  • [ ] Cancel single occurrence (create cancelled exception)
  • [ ] Create new exception that is already cancelled

🟠 Partial Attendee Lists (EXDATE Logic)

Critical EXDATE Scenarios

  • [ ] Remove attendee from single occurrence (verify EXDATE in their REQUEST)
  • [ ] Add attendee to single occurrence only (verify they get only exception)
  • [ ] Different attendees on different exceptions:
    • [ ] Master: A + B + C
    • [ ] Exception 1: A + B only
    • [ ] Exception 2: A + C only
    • [ ] Verify A gets master + both exceptions
    • [ ] Verify B gets master + exception 1 + EXDATE for exception 2
    • [ ] Verify C gets master + exception 2 + EXDATE for exception 1

🔴 Edge Cases

Organizer & Empty States

  • [ ] Organizer is also an attendee (verify organizer doesn't receive message)
  • [ ] Create event with no attendees
  • [ ] Remove all attendees from event

✅ Verification Per Test

For each test above, verify:

Message Method

  • [ ] REQUEST for updates/additions
  • [ ] CANCEL for removals/cancellations

Recipients

  • [ ] All appropriate attendees receive messages
  • [ ] No duplicate messages sent
  • [ ] Organizer doesn't receive self-invites
  • [ ] SCHEDULE-AGENT=CLIENT attendees skipped

Message Content

  • [ ] All properties correctly reflected
  • [ ] EXDATE properly added for partial attendee lists

Instance Selection

  • [ ] Master included when appropriate
  • [ ] Exceptions included when appropriate
  • [ ] Cancelled instances not in REQUEST (only in CANCEL)

TODO

  • [x] Add phpdoc to function
  • [x] Add phpdoc parameter definitions for arrays

Checklist

SebastianKrupinski avatar Feb 17 '25 00:02 SebastianKrupinski

Is this intended?

I don't think this has anything to do with this PR.

  1. Organizer invites attendee (recurring, daily, 5 times).

This should work just like any other scenario, I tested this on my end and the iTip message are being generated properly.

Example,

Screenshot 2025-02-24 140336

Screenshot 2025-02-24 140413

  1. Attendee declines the second to last instance.

This PR does not touch the Attendee portion of the iTipBroker, that is done in the parseEventForAttendee function and this only overloads the parseEventForOrganizer function which generates messages for organizer changes.

In general, no messages seem to get sent if an attendee accepts or declines a single instance.

I had the same problem, this is most likely a issue with your DEV instance, when our server tests run they empty out the appdata_* folder, this is the place where the contents of a sent message is saved and if the "appdata_/mail/mail_user" folder is missing the send process fails.

Screenshot 2025-02-24 140655

Screenshot 2025-02-24 140949

Screenshot 2025-02-24 141349

SebastianKrupinski avatar Feb 24 '25 19:02 SebastianKrupinski

/backport to stable31

st3iny avatar Feb 25 '25 16:02 st3iny

/backport to stable30

st3iny avatar Feb 25 '25 16:02 st3iny

Does not fix cancelling a single instance.

Consider two accounts: organizer and user.

1. Organizer: Create recurring event, daily, 3 times and invite another user.

2. User: Accept whole series.

3. Organizer: Cancels an instance, e.g. the second one.

Expected: User will only see the second instance being cancelled. Actual: The whole series is cancelled from user's POV.

I exported the events and was able to confirm that the organizer's event has a new instance with a cancelled state. The user's event however, does not contain a separate instance. Instead, the base event was cancelled.

Okay, having a look. Disregard the last message, I tested the wrong thing

SebastianKrupinski avatar Mar 03 '25 14:03 SebastianKrupinski

Does not fix cancelling a single instance.

Consider two accounts: organizer and user.

1. Organizer: Create recurring event, daily, 3 times and invite another user.

2. User: Accept whole series.

3. Organizer: Cancels an instance, e.g. the second one.

Expected: User will only see the second instance being cancelled. Actual: The whole series is cancelled from user's POV.

I exported the events and was able to confirm that the organizer's event has a new instance with a cancelled state. The user's event however, does not contain a separate instance. Instead, the base event was cancelled.

LMAO. So I found the issue... You've found another bug that I didn't know existed...

So the iTipBroker is now generating proper messages for instances, but its not processing Cancellation instances properly...

The method at fault... Sabre\VObject\ITip\Broker::processMessageCancel()

    protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null)
    {
        if (!$existingObject) {
            // The event didn't exist in the first place, so we're just
            // ignoring this message.
        } else {
            foreach ($existingObject->VEVENT as $vevent) {
                $vevent->STATUS = 'CANCELLED';
                $vevent->SEQUENCE = $itipMessage->sequence;
            }
        }

        return $existingObject;
    }

As you can see it does not check the iTipMessage for a RECURRANCE-ID, it just applies the cancellation to every instance...

SebastianKrupinski avatar Mar 03 '25 16:03 SebastianKrupinski

Remove EXDATE from master

How do I do this from the Calendar UI?

ChristophWurst avatar Nov 10 '25 07:11 ChristophWurst

Remove EXDATE from master

How do I do this from the Calendar UI?

That one you actually can't do through the UI, we don't support that, it can be skipped, or you can do a manual PUT request to trigger it.

SebastianKrupinski avatar Nov 10 '25 13:11 SebastianKrupinski