Skip to content

Send the meeting response only once the meeting is stored - #11

Open
r4nc0r wants to merge 1 commit into
grommunio:masterfrom
r4nc0r:fix/response-sent-before-meeting-saved
Open

Send the meeting response only once the meeting is stored#11
r4nc0r wants to merge 1 commit into
grommunio:masterfrom
r4nc0r:fix/response-sent-before-meeting-saved

Conversation

@r4nc0r

@r4nc0r r4nc0r commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The problem

Meetingrequest::accept() creates and submits the response as its first act, before any calendar work:

if ($sendresponse) {
    $this->createResponse(...);      // response is submitted here
}
// everything below runs with the acceptance already delivered:
//   the recurring branch and acceptException()
//   mapi_folder_createmessage() + mapi_savechanges() for the calendar item
//   in the $move path, deletion of the previous calendar item before recreating it

Any failure after that point leaves the organizer holding an acceptance for a meeting the attendee does not have, and the attendee with nothing in the calendar. The window is not small: it spans the whole recurring branch, acceptException(), and a mapi_folder_deletemessages() on the attendee's existing calendar item that happens before the replacement is created, so a failure there can also remove a meeting the attendee already had.

Insufficient calendar rights are not affected — ensureCalendarWriteAccess() throws before the response is built — but every other failure in that window is.

Why the response cannot simply be built later

It is assembled from the request message: createResponse() reads PR_SENT_REPRESENTING_*, the GlobalObjectIds, subject, start/due, recurrence pattern and timezone from $this->message, and constructs a Recurrence over it. In the $move path that message is copied into the calendar and the original removed, so those reads have to happen while it is still intact.

The change

Keep building the response where it was; stop short of submitting it.

  • createResponse() gains a bool $defer = false parameter. When set it returns the prepared, saved-but-unsubmitted message instead of calling mapi_message_submitmessage(). Both existing call sites are internal, and doDecline() is untouched.
  • accept() stores that message and submits it at the end, once the calendar item exists.
  • An accept that stores nothing returns an empty entryid. That case now withholds the response too, rather than reporting an acceptance for a meeting that was never created.
  • doAccept() wraps the two accept() calls in try/finally and discards any still-pending response, so an unsent message never stays behind in the Outbox when accept() throws. createOutgoingMessage() builds it in the Outbox, so leaving it there would show the user a stuck item.

Net effect: the failure mode changes from "response sent, meeting lost" to "nothing happened", which is recoverable — the request mail is still in the inbox and the user can accept again.

Testing

On a gromox test rig (php-mapi, real store), driving the same sequence class.itemmodule.php uses for acceptMeetingRequest, with a failure injected immediately after createResponse():

organizer responses calendar item request mail Outbox
before the change 15 → 16 not created still in inbox
after the change 17 → 17 not created still in inbox no unsent response

accept() created and submitted the response as its first act, before any
calendar work. Everything after that - creating the calendar item, the
recurring branch, acceptException(), and in the $move path deleting the
previous item before recreating it - therefore runs with the acceptance
already delivered. Any failure in that window leaves the organiser with
an acceptance for a meeting the attendee does not have, and the attendee
with no appointment.

The response cannot simply be built later: it is assembled from the
request message, which the $move path copies into the calendar and then
removes. So keep building it where it was, but stop short of submitting.
createResponse() gains a $defer flag and returns the prepared message,
accept() submits it after the calendar item exists, and doAccept() drops
it in a finally block if accept() threw, so an unsent response never
stays behind in the Outbox.

An accept that stores nothing returns an empty entryid, and that case now
withholds the response as well rather than reporting success.

Measured on a gromox test rig with a failure injected immediately after
createResponse(): before, the organiser's response count went 15 to 16
with no calendar item; after, it stays at 17, the request mail is still
in the inbox, and the Outbox holds no unsent response. The ordinary
accept path is unchanged, including when the invitation conflicts with an
existing appointment.
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.

1 participant