Using Exchange Web Service with Powershell to remove calendar appointments without notification

While helping out a client recently something in their migration went wrong, I ran into an interesting challenge. Calendars of users were merged, many many times. The resulting duplicates that shouldn’t be in user’s calendars could be identified easily: the mailbox was neither the organiser nor invited to these calendar events.

But how to remove them? Graph won’t allow you to do so without notifying the recipients (leading, potentially, to thousands of confused users).

Luckily, EWS DOES allow us to do so, and if you ever need to work with EWS (Office 365 Exchange Online) using Powershell, this code sample could come in handy 🙂

Add-Type -Path "C:\Users\jos\Desktop\net35\Microsoft.Exchange.WebServices.dll"

$Service = [Microsoft.Exchange.WebServices.Data.ExchangeService]::new()
$Service.Credentials = [System.Net.NetworkCredential]::new("admin@onedrivemapper.onmicrosoft.com" , "yourpassword")
$Service.Url = "https://outlook.office365.com/EWS/Exchange.asmx"

$maxDaysIntoTheFuture = 365

function Remove-ObsoleteCalendarItems{
    Param(
        $primaryEmailAddress #eg: admin@onedrivemapper.onmicrosoft.com
    )

    $folderid= new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar,$primaryEmailAddress)   
    $Calendar = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($Service,$folderid)
    $Recurring = new-object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Appointment, 0x8223,[Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Boolean); 
    $psPropset= new-object Microsoft.Exchange.WebServices.Data.PropertySet([Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties)  
    $psPropset.Add($Recurring)
    $psPropset.RequestedBodyType = [Microsoft.Exchange.WebServices.Data.BodyType]::Text;

    #Define Date to Query 
    $currentDay = 0
    while($True){
        $StartDate = (Get-Date).AddDays($currentDay)
        $EndDate = $StartDate.AddDays(14)  
        $currentDay += 14

        if($currentDay -gt $maxDaysIntoTheFuture){
            break
        }

        $CalendarView = New-Object Microsoft.Exchange.WebServices.Data.CalendarView($StartDate,$EndDate,1000)    
        $fiItems = $service.FindAppointments($Calendar.Id,$CalendarView)
        if($fiItems.Items.Count -gt 0){
            $type = ("System.Collections.Generic.List"+'`'+"1") -as "Type"
            $type = $type.MakeGenericType("Microsoft.Exchange.WebServices.Data.Item" -as "Type")
            $ItemColl = [Activator]::CreateInstance($type)
            foreach($Item in $fiItems.Items){
                $ItemColl.Add($Item)
            } 
            [Void]$service.LoadPropertiesForItems($ItemColl,$psPropset)  
        }

        foreach($Item in $fiItems.Items){  
            if($Item.Organizer.Address -ne $primaryEmailAddress -and $Item.RequiredAttendees.Address -notcontains $primaryEmailAddress -and $Item.OptionalAttendees.Address -notcontains $primaryEmailAddress){
                $Item.RequiredAttendees.Clear() #this also works if no one is invited
                $Item.OptionalAttendees.Clear() #this also works if no one is invited
                $Item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite,[Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone)
                $Item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::MoveToDeletedItems)
                write-host "deleted item $($Item.Subject) without notifying recipients"
            }
        }
    }
}

The required EWS DLL’s can also be found in my git repository:

Delete User Profiles Older than a Specified Number of Days on System Restart through Intune

The good old Group Policy “Configuration\Policies\Administrative Templates\System\User Profiles\Delete User Profiles Older than a Specified Number of Days on System Restart ” isn’t part of Intune yet.

If you use shared devices in your environment, you can use below script to set the number of days after which a user profile is cleaned up on Windows 10 MDM / Intune managed.

It has to run under SYSTEM context or it won’t be allowed to write the right key.

Download: https://gitlab.com/Lieben/assortedFunctions/blob/master/set-CleanupUserProfilesAfterDays.ps1

hidden exceptions to conditional access MFA

EDIT: this no longer works and results in an UnSupportedFirstyPartyApplication or ServicePrincipalNotFound error.

If you set an Intune conditional access policy to target ALL applications in Azure AD with MFA, a new Windows 10 device will not be able to fully install, and will never become usable for the user.

This is because your client needs to connect to Azure AD endpoints such as the Graph API ( 00000002-0000-0000-c000-000000000000 ) and the Store for Business (45a330b1-b1ec-4cc1-9161-9f03992aa49f).

As these applications cannot be excluded from a conditional access policy through the GUI, you’ll have to use Fiddler:

  1. Configure your policy, but don’t click ‘save’
  2. Start Fiddler, enable SSL decrytion
  3. Start monitoring in fiddler
  4. Click ‘save’ in the conditional acccess policy
  5. Stop monitoring in Fiddler
  6. Look for the ‘put’ request, and copy it to notepad or directly to the ‘raw’ tab in the fiddler composer section
  7. Modify the app id’s in the request, e..g. : “servicePrincipals”:{“allServicePrincipals”:1,”included”:{“ids”:[]},”excluded”:{“ids”:[“0000000a-0000-0000-c000-000000000000″,”d4ebce55-015a-49b5-a083-c84d1797ae8c”,”45a330b1-b1ec-4cc1-9161-9f03992aa49f”,”00000002-0000-0000-c000-000000000000″]}
  8. Send the request
  9. DO NOT EDIT THE POLICY AGAIN THROUGH THE INTUNE PORTAL. Otherwise your hidden id’s will be removed
Composer tab in Fiddler
resulting message in the compliance policy in Intune

Obviously the above method is not supported by Microsoft!

To get some google hits, these are some of the many errors that will hit your eventlog if you didn’t do the above.

Error: 0xCAA2000C The request requires user interaction.

Code: interaction_required

Description: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access ‘00000002-0000-0000-c000-000000000000’.

Trace ID: 84d6797d-00b8-49e4-a4cf-aab381ac9400

Correlation ID: 9a3788e5-52cc-44a4-a468-f64816ad06d6

Timestamp: 2019-10-08 12:45:19Z

TokenEndpoint: https://login.microsoftonline.com/common/oauth2/token

Logged at oauthtokenrequestbase.cpp, line: 409, method: OAuthTokenRequestBase::ProcessOAuthResponse.

Request: authority: https://login.microsoftonline.com/common, client: fc0f3af4-6835-4174-b806-f7db311fd2f3, redirect URI: ms-appx-web://Microsoft.AAD.BrokerPlugin/fc0f3af4-6835-4174-b806-f7db311fd2f3, resource: 00000002-0000-0000-C000-000000000000, correlation ID (request): 9a3788e5-52cc-44a4-a468-f64816ad06d6

Error: 0xCAA2000C The request requires user interaction.

Code: interaction_required

Description: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access ‘https://substrate-dod-int.office365.us/‘.

Trace ID: df1ee58f-da80-4a25-897c-51b1a7639700

Correlation ID: 946be868-d70e-4670-a4ea-8dac9b05fa17

Timestamp: 2019-10-08 12:39:06Z

TokenEndpoint: https://login.microsoftonline.com/common/oauth2/token

Logged at oauthtokenrequestbase.cpp, line: 409, method: OAuthTokenRequestBase::ProcessOAuthResponse.

Request: authority: https://login.microsoftonline.com/common, client: 26a7ee05-5602-4d76-a7ba-eae8b7b67941, redirect URI: ms-appx-web://Microsoft.AAD.BrokerPlugin/S-1-15-2-1861897761-1695161497-2927542615-642690995-327840285-2659745135-2630312742, resource: https://substrate.office.com, correlation ID (request): 946be868-d70e-4670-a4ea-8dac9b05fa17

Error: 0xCAA2000C The request requires user interaction.

Code: interaction_required

Description: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access ‘8f41dc7c-542c-4bdd-8eb3-e60543f607ca’.

Trace ID: 4ba9b18c-86bb-43c9-a26b-5a3ee0a46700

Correlation ID: 797c75ce-46f5-436c-9b53-834ce030a6cf

Timestamp: 2019-10-08 13:06:43Z

TokenEndpoint: https://login.microsoftonline.com/common/oauth2/token

Logged at oauthtokenrequestbase.cpp, line: 409, method: OAuthTokenRequestBase::ProcessOAuthResponse.

Request: authority: https://login.microsoftonline.com/common, client: {6F7E0F60-9401-4F5b-98E2-CF15BD5Fd5E3}, redirect URI: ms-appx-web://Microsoft.AAD.BrokerPlugin/{6F7E0F60-9401-4F5b-98E2-CF15BD5Fd5E3}, resource: https://cs.dds.microsoft.com, correlation ID (request): 797c75ce-46f5-436c-9b53-834ce030a6cf

Error: 0xCAA2000C The request requires user interaction.

Code: interaction_required

Description: AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access ‘d32c68ad-72d2-4acb-a0c7-46bb2cf93873’.

Trace ID: 57216986-a61a-4fbe-8f4a-6516a4da7800

Correlation ID: 4fe8fddb-3fbd-488d-82da-73286d556d85

Timestamp: 2019-10-08 13:06:08Z

TokenEndpoint: https://login.microsoftonline.com/common/oauth2/token

Logged at oauthtokenrequestbase.cpp, line: 409, method: OAuthTokenRequestBase::ProcessOAuthResponse.

Request: authority: https://login.microsoftonline.com/common, client: {6F7E0F60-9401-4F5b-98E2-CF15BD5Fd5E3}, redirect URI: ms-appx-web://Microsoft.AAD.BrokerPlugin/{6F7E0F60-9401-4F5b-98E2-CF15BD5Fd5E3}, resource: https://activity.microsoft.com, correlation ID (request): 4fe8fddb-3fbd-488d-82da-73286d556d85