Articles in the "Meeting cancellation script" series

One of the long-running woes of the Exchange admin is that you can’t transfer “ownership” of a meeting to someone else when the owner leaves the company. While you still can’t do that, Microsoft recently announced the Remove-CalendarEvents cmdlet for mailboxes in Exchange Online. Its intention is to allow the administrator to cancel all future meetings for a terminated employee so that someone else can create new meetings in their place.

Because it works only for cloud mailboxes, I wrote a script to do the same thing for on-premises mailboxes (though it will also work for cloud mailboxes). It uses EWS to get all future meetings for a mailbox and cancel them. The default settings are to cancel all meetings that occur in the next year where the mailbox is the organizer. You can also choose to have it cancel (decline) meetings where the mailbox is an attendee, and you can specify text that should be added to the cancellation or decline message. Additionally, if you don’t want to wholly cancel recurring meetings because the history of all those occurrences will be lost for the attendees, you can instead have organized recurring meetings end so that only future occurrences are canceled, but historical occurrences remain.

Because of the way calendar items are stored, you can get an item and then see when it occurs, if it is recurring, etc., but if it is a recurring meeting you only see the first occurrence. To work with occurrences, you use a calendar view (time frame) and let Exchange worry about whether an occurrence of a recurring item should be included in the window. But you can have multiple occurrences of the same meeting in the time frame, and you want to delete the series, not an occurrence. In a calendar view, you can see if a meeting is an occurrence and, if so, get the corresponding master for that series.

Once the series is canceled (or ended, if you choose that option), there still can be more occurrences of that canceled series in the search results. If you try and get the recurring master for an occurrence whose master has already been canceled, you’ll get an error. So, these occurrences can be skipped, but you have to know which ones can be skipped. To solve that, I store the recurring meeting’s global object ID (which is the same for a recurring master and all of its occurrences because it is really just one meeting as stored in the calendar) in an array. For every meeting in the search results that is an occurrence, I check if the global object ID is in the array. If so, I skip it because its master has already been deleted. If not, I add the global object ID to the array, then cancel the meeting.

The script has comment-based help so you know what all the parameters are for, and there are inline comments so you can see what is being done throughout. While you can use current credentials or specify one, if you want it to prompt if you don’t specify credentials and don’t want to use default credentials, you can comment and uncomment the the necessary lines at 81-84. Likewise for the EWS URL, it is hard-coded to EXO, which you can change, but if you want to use autodiscover, you can comment and uncomment the necessary lines at 91-93.

Download the script via the link below, but you can see the code and copy it below, too.

elseif($PSCmdlet.ShouldProcess("Meeting series with the subject `"$($master.Subject)`" that has one or more past occurrence exceptions that will be lost if the series`' end date is changed")-or$SuppressLostExceptionPrompt)

{

Update-MeetingEndDate-meeting$master

}

}

else

{

Update-MeetingEndDate-meeting$master

}

}

else

{

Send-MeetingCancel-meeting$master'Series'

}

}

else

{

Send-MeetingDecline-meeting$master'Series'

}

}

}

else#Non-recurring meeting

{

if($attendeeType-eq"Organizer")

{

Send-MeetingCancel-meeting$meeting'Meeting'

}

else

{

Send-MeetingDecline-meeting$meeting'Meeting'

}

}

}

functionUpdate-MeetingEndDate($meeting)

{

if($PreviewOnly)

{

Write-Output"Series whose end date would be updated: `"$($meeting.Subject)`" on $($meeting.Start.ToShortDateString())"