# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. #Requires -Version 3.0 <# .SYNOPSIS This script audits mails, calendar and task items and check if PidLidReminderFileParameter property is populated or not. If required admins can use this script to cleanup the property for items they find malicious or even delete these items permanently. .DESCRIPTION There are two modes in which we can run the script Audit and Cleanup. Audit Mode: Script provides a csv to the admins with details of items that have PidLidReminderFileParameter property populated. Cleanup Mode: Script performs Cleanup action on items by either clearing the property or deleting the mail itself. Prerequisites to run the script for Exchange Server on-premises: You need to have ApplicationImpersonation role. You can create a new role group with the required permissions by running the following PowerShell command in an elevated Exchange Management Shell (EMS): `New-RoleGroup -Name "CVE-2023-23397-Script" -Roles "ApplicationImpersonation" -Description "Permission to run the CVE-2023-23397 script"` Prerequisites to run the script for Exchange Online: You need to be a member of Organization Management. The script will create an application with full access permission on all the mailboxes. The script uses EWS managed api to make ews calls in order to fetch items from user mailboxes. So the machine on which the script is run should be able to make EWS calls to Exchange Server. .PARAMETER CreateAzureApplication Use this switch to create a Azure AD application that can be used for running the script in online mode .PARAMETER DeleteAzureApplication Use this switch to delete the Azure AD application .PARAMETER UserMailboxes Use this parameter to provide list of user primary SMTP address. .PARAMETER StartTimeFilter Use this parameter to provide start time filter .PARAMETER EndTimeFilter Use this parameter to provide end time filter .PARAMETER CleanupAction Use this parameter to provide type of cleanup action you want to provide (ClearProperty/ClearItem) .PARAMETER CleanupInfoFilePath Use this parameter to provide path to the csv file containing the details of items to be cleaned up .PARAMETER EWSExchange2013 Use this switch if you are running on Exchange 2013 server mailboxes .PARAMETER MaxCSVLength This optional parameter allows you to provide maximum csv length .PARAMETER AzureEnvironment This optional parameter allows you to provide Azure environment .PARAMETER AzureApplicationName This optional parameter allows you to provide Azure application name .PARAMETER CertificateThumbprint This optional parameter allows you to provide a certificate thumbprint to use the Azure application created by the script .PARAMETER AppId This optional parameter allows you to provide the AppId of the Azure application created by the script .PARAMETER Organization This optional parameter allows you to provide the organization name e.g., contoso.onmicrosoft.com .PARAMETER AzureADEndpoint This optional parameter allows you to provide Azure AD endpoint .PARAMETER EWSServerURL This optional parameter allows you to provide EWS server URL .PARAMETER DLLPath This optional parameter allows you to provide the path to Microsoft.Exchange.WebServices.dll .PARAMETER ScriptUpdateOnly This optional parameter allows you to only update the script without performing any other actions. .PARAMETER SkipVersionCheck This optional parameter allows you to skip the automatic version check and script update. .PARAMETER IgnoreCertificateMismatch This optional parameter lets you ignore TLS certificate mismatch errors. .PARAMETER Credential This optional parameter lets you pass administrator credential object while running on Exchange Server on-premises. .PARAMETER UseSearchFolders This switch causes the script to use deep-traversal search folders, significantly improving performance. .PARAMETER SearchFolderCleanup Clean up any search folders left behind by the -UseSearchFolders switch. .PARAMETER SkipSearchFolderCreation Skip the first pass of -UseSearchFolders and just check the existing search folders for results. .PARAMETER TimeoutSeconds This optional parameter lets you specify the timeout value for the ExchangeService object. Defaults to 5 minutes. .EXAMPLE PS C:\> .\CVE-2023-23397.ps1 -CreateAzureApplication This will run the tool to create a new Azure application with required permissions .EXAMPLE PS C:\> (Get-Mailbox).PrimarySMTPAddress | .\CVE-2023-23397.ps1 -Environment This will run the tool in audit mode on all the users present .EXAMPLE PS C:\> .\CVE-2023-23397.ps1 -Environment -CleanupAction ClearItem -CleanupInfoFilePath This will run the tool in clean up mode and clear all the items mentioned in the csv file #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [ValidateSet("Online", "Onprem")] [Parameter(Mandatory = $true, ParameterSetName = "Audit")] [Parameter(Mandatory = $true, ParameterSetName = "Cleanup")] [string]$Environment, [Parameter(Mandatory = $true, ParameterSetName = "CreateAzureApplication")] [switch]$CreateAzureApplication, [Parameter(Mandatory = $true, ParameterSetName = "DeleteAzureApplication")] [switch]$DeleteAzureApplication, [Parameter(Mandatory = $true, ParameterSetName = "Audit", ValueFromPipelineByPropertyName = $true)] [Alias("PrimarySmtpAddress")] [string[]]$UserMailboxes, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [DateTime]$StartTimeFilter, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [DateTime]$EndTimeFilter, [ValidateSet("ClearProperty", "ClearItem")] [Parameter(Mandatory = $true, ParameterSetName = "Cleanup")] [string]$CleanupAction, [ValidateScript({ Test-Path -Path $_ -PathType leaf })] [Parameter(Mandatory = $true, ParameterSetName = "Cleanup")] [ValidatePattern("(.*?)\.(csv)$")] [string]$CleanupInfoFilePath, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [switch]$EWSExchange2013, [ValidateRange(1, [Int]::MaxValue)] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [int]$MaxCSVLength = 200000, [ValidateSet("Global", "USGovernmentL4", "USGovernmentL5", "ChinaCloud", "BleuCloud", "DelosCloud")] [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$AzureEnvironment = "Global", [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$AzureApplicationName = "CVE-2023-23397Application", [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$CertificateThumbprint, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$AppId, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$Organization, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [uri]$EWSServerURL, [ValidateScript({ Test-Path $_ })] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [string]$DLLPath, [Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly")] [switch]$ScriptUpdateOnly, [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [switch]$SkipVersionCheck, [Parameter(Mandatory = $false, ParameterSetName = "CreateAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "DeleteAzureApplication")] [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [switch]$IgnoreCertificateMismatch, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [switch]$UseSearchFolders, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [switch]$SearchFolderCleanup, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [switch]$SkipSearchFolderCreation, [Parameter(Mandatory = $false, ParameterSetName = "Audit")] [Parameter(Mandatory = $false, ParameterSetName = "Cleanup")] [ValidateRange(1, 2147483)] [int]$TimeoutSeconds = 300 ) dynamicparam { if ($Environment -eq "Onprem") { $auditParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Audit" Mandatory = $true } $cleanupParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Cleanup" Mandatory = $true } } else { $auditParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Audit" Mandatory = $false } $cleanupParameterAttribute = [System.Management.Automation.ParameterAttribute]@{ ParameterSetName = "Cleanup" Mandatory = $false } } $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $attributeCollection.Add($auditParameterAttribute) $attributeCollection.Add($cleanupParameterAttribute) $credentialType = [System.Management.Automation.PSCredential] $dynParameter = New-Object System.Management.Automation.RuntimeDefinedParameter 'Credential', $credentialType, $attributeCollection $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $paramDictionary.Add('Credential', $dynParameter) return $paramDictionary } begin { $BuildVersion = "" $searchFolderName = "ReminderFileParameterItems" $credential = $PSBoundParameters['Credential'] . $PSScriptRoot\WriteFunctions.ps1 . $PSScriptRoot\..\..\..\Shared\CertificateFunctions\Enable-TrustAnyCertificateCallback.ps1 . $PSScriptRoot\..\..\..\Shared\OutputOverrides\Write-Host.ps1 . $PSScriptRoot\..\..\..\Shared\OutputOverrides\Write-Verbose.ps1 . $PSScriptRoot\..\..\..\Shared\OutputOverrides\Write-Warning.ps1 . $PSScriptRoot\..\..\..\Shared\ScriptUpdateFunctions\Test-ScriptVersion.ps1 . $PSScriptRoot\..\..\..\Shared\Get-NuGetPackage.ps1 . $PSScriptRoot\..\..\..\Shared\Invoke-ExtractArchive.ps1 . $PSScriptRoot\..\..\..\Shared\LoggerFunctions.ps1 . $PSScriptRoot\..\..\..\Shared\ActiveDirectoryFunctions\Test-ADCredentials.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-GraphAccessToken.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-CloudServiceEndpoint.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-NewJsonWebToken.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Get-NewOAuthToken.ps1 . $PSScriptRoot\..\..\..\Shared\AzureFunctions\Invoke-GraphApiRequest.ps1 . $PSScriptRoot\..\..\..\Shared\GraphApiFunctions\Remove-AzureApplication.ps1 . $PSScriptRoot\..\..\..\Shared\GraphApiFunctions\Get-AzureApplication.ps1 . $PSScriptRoot\..\..\..\Shared\GraphApiFunctions\New-EwsAzureApplication.ps1 . $PSScriptRoot\..\..\..\Shared\GraphApiFunctions\New-AzureApplicationAppSecret.ps1 . $PSScriptRoot\..\..\..\Shared\Show-Disclaimer.ps1 $loggerParams = @{ LogName = "CVE-2023-23397-$((Get-Date).ToString("yyyyMMddhhmmss"))-Debug" AppendDateTimeToFileName = $false ErrorAction = "SilentlyContinue" } $Script:Logger = Get-NewLoggerInstance @loggerParams SetWriteHostAction ${Function:Write-HostLog} SetWriteVerboseAction ${Function:Write-VerboseLog} SetWriteWarningAction ${Function:Write-HostLog} $mode = $PsCmdlet.ParameterSetName $cloudService = Get-CloudServiceEndpoint $AzureEnvironment # Define the endpoints that we need for the various calls to the Azure AD Graph API and EWS $ewsOnlineURL = "$($cloudService.ExchangeOnlineEndpoint)/EWS/Exchange.asmx" $ewsOnlineScope = "$($cloudService.ExchangeOnlineEndpoint)/.default" $autoDSecureName = $cloudService.AutoDiscoverSecureName $azureADEndpoint = $cloudService.AzureADEndpoint $graphApiEndpoint = $cloudService.GraphApiEndpoint ## function to create ews managed api service object function EWSAuth { param( [string]$Environment, $Token, $EWSOnlineURL, $EWSServerURL ) ## Create the Exchange Service object with credentials if ($EWSExchange2013) { $Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013) } else { $Service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2016) } $Service.Timeout = $TimeoutSeconds * 1000 if ($Environment -eq "Onprem") { $Service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.WebCredentials($credential.UserName, [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password))) } else { $Service.Credentials = New-Object Microsoft.Exchange.WebServices.Data.OAuthCredentials($Token.access_token) } if ($Environment -eq "Onprem") { if (-not([System.String]::IsNullOrEmpty($EWSServerURL))) { $Service.Url = New-Object Uri($EWSServerURL) CheckOnpremCredentials -EWSService $Service } else { try { $credentialTestResult = Test-ADCredentials -Credentials $credential if ($credentialTestResult.CredentialsValid) { Write-Host "Credentials validated successfully." -ForegroundColor Green } elseif ($credentialTestResult.CredentialsValid -eq $false) { Write-Host "Credentials that were provided are incorrect." -ForegroundColor Red exit } else { Write-Host "Credentials couldn't be validated. Trying to use the credentials anyway." -ForegroundColor Yellow } if (($credentialTestResult.UsernameFormat -eq "downlevel") -or ($credentialTestResult.UsernameFormat -eq "local")) { Write-Host "Username: $($Credential.UserName) was passed in $($credentialTestResult.UsernameFormat) format" -ForegroundColor Red Write-Host "You must use the -EWSServerURL parameter if the username is not in UPN (username@domain) format." -ForegroundColor Red Write-Host "More information: https://aka.ms/CVE-2023-23397ScriptDoc" -ForegroundColor Red exit } $redirectionCallback = { param([string]$url) return $url.ToLower().StartsWith($autoDSecureName) } $Service.AutodiscoverUrl($Credential.UserName, $redirectionCallback) } catch [Microsoft.Exchange.WebServices.Data.AutodiscoverLocalException] { Write-Host "Unable to locate the Autodiscover service by using username $($Credential.UserName)" -ForegroundColor Red Write-Host "A reason could be that the username that was passed uses a domain which is not an accepted domain by Exchange Server." -ForegroundColor Red Write-Host "You must use the -EWSServerURL parameter if the Autodiscover service cannot be located." -ForegroundColor Red Write-Host "More information: https://aka.ms/CVE-2023-23397ScriptDoc" -ForegroundColor Red Write-Host "Inner Exception:`n$_" -ForegroundColor Red exit } catch [System.FormatException] { # We should no longer get here as we are validating the username format above, however, keeping this as failsafe for now Write-Host "Username: $($Credential.UserName) was passed in an unexpected format" -ForegroundColor Red Write-Host "Please try again or use the -EWSServerURL parameter to provide the EWS url." -ForegroundColor Red Write-Host "More information: https://aka.ms/CVE-2023-23397ScriptDoc" -ForegroundColor Red Write-Host "Inner Exception:`n$_" -ForegroundColor Red exit } catch { Write-Host "Unable to make Autodiscover call to fetch EWS endpoint details. Please make sure you have enter valid credentials. Inner Exception`n`n$_" -ForegroundColor Red exit } } } else { $Service.Url = $EWSOnlineURL } return $Service } ## function to validate onprem credential function CheckOnpremCredentials { param( $EWSService ) try { $null = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($EWSService, [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot) } catch [Microsoft.Exchange.WebServices.Data.ServiceRequestException] { Write-Host "Unable to connect to EWS endpoint. Please make sure you have enter valid credentials. Inner Exception`n`n$_" -ForegroundColor Red exit } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { if ($_.Exception.ErrorCode -eq "ErrorNonExistentMailbox") { # This is fine. This means this account is not mail-enabled, but credentials worked. return } elseif ($_.Exception.ErrorCode -eq "ErrorMissingEmailAddress") { # On Exchange 2013, even if we handle this error, it doesn't work. Write-Host "Could not open mailbox due to ErrorMissingEmailAddress. If running on Exchange 2013, the impersonation user must have a mailbox on prem. Inner Exception`n`n$_" -ForegroundColor Red exit } else { Write-Host "Could not open mailbox. Inner Exception`n`n$_" -ForegroundColor Red exit } } catch { Write-Host "Could not open mailbox. Inner Exception`n`n$_" -ForegroundColor Red exit } } ## function to add a row to the csv function CreateCustomCSV { param( $Mailbox, $Data, [string]$CsvPath ) $ItemType = $Data.ItemClass if ($Data.ItemClass.StartsWith("IPM.Note")) { $ItemType = "E-Mail" } elseif ($Data.ItemClass.StartsWith("IPM.Appointment")) { $ItemType = "Calendar" } elseif ($Data.ItemClass.StartsWith("IPM.Task")) { $ItemType = "Task" } <# Make sure to update the $headerLine.Count check if you add more columns or remove some. We do the check after running Import-Csv call before processing a csv file that was passed via CleanupInfoFilePath parameter. #> $row = [PSCustomObject]@{ "Mailbox" = $Mailbox "Id" = $Data.Id "ItemType" = $ItemType "Sender" = ($Data.From | Select-Object -ExpandProperty Address) -join "," "Recipient" = ($Data.ToRecipients | Select-Object -ExpandProperty Address) -join "," "Subject" = $Data.Subject "DateReceived" = $Data.DateTimeReceived "PidLidReminderFileParameter" = $Data.ExtendedProperties[0].Value "Cleanup" = "N" } $row | Export-Csv -Path $CsvPath -Delimiter "," -NoTypeInformation -Append -Encoding utf8 -Force } # Define a function to get all the SubFolders of a given folder function GetSubFolders { param( $Folder, $FoldersList ) # Get the SubFolders of the folder $folderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(100) do { $Folder.FindFolders($folderView) | ForEach-Object { # Add the folder path to the list $null = $FoldersList.Add($_) # Recursively get the SubFolders of this folder GetSubFolders -folder $_ -foldersList $FoldersList } $folderView.Offset += $result.Folders.Count } while ($result.MoreAvailable -eq $true) } ## function to find item on basis of store ID function FindItem { param( [Microsoft.Exchange.WebServices.Data.ExchangeService]$ExchangeService, [string]$Id ) $ps = New-Object Microsoft.Exchange.WebServices.Data.PropertySet(New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common, 0x0000851F, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String)) return [Microsoft.Exchange.WebServices.Data.Item]::Bind($ExchangeService, $Id, $ps) } ## function to check if a given token is expired and renew it function CheckTokenExpiry { param( $ApplicationInfo, [ref]$EWSService, [ref]$Token, [string]$Environment, $EWSOnlineURL, $EWSOnlineScope, $AzureADEndpoint ) if ($Environment -eq "Onprem") { return } # if token is going to expire in next 5 min then refresh it if ($null -eq $script:tokenLastRefreshTime -or $script:tokenLastRefreshTime.AddMinutes(55) -lt (Get-Date)) { $createOAuthTokenParams = @{ TenantID = $ApplicationInfo.TenantID ClientID = $ApplicationInfo.ClientID Endpoint = $AzureADEndpoint CertificateBasedAuthentication = (-not([System.String]::IsNullOrEmpty($ApplicationInfo.CertificateThumbprint))) Scope = $EWSOnlineScope } # Check if we use an app secret or certificate by using regex to match Json Web Token (JWT) if ($ApplicationInfo.AppSecret -match "^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$") { $jwtParams = @{ CertificateThumbprint = $ApplicationInfo.CertificateThumbprint CertificateStore = "CurrentUser" Issuer = $ApplicationInfo.ClientID Audience = "$AzureADEndpoint/$($ApplicationInfo.TenantID)/oauth2/v2.0/token" Subject = $ApplicationInfo.ClientID } $jwt = Get-NewJsonWebToken @jwtParams if ($null -eq $jwt) { Write-Host "Unable to sign a new Json Web Token by using certificate: $($ApplicationInfo.CertificateThumbprint)" -ForegroundColor Red exit } $createOAuthTokenParams.Add("Secret", $jwt) } else { $createOAuthTokenParams.Add("Secret", $ApplicationInfo.AppSecret) } $oAuthReturnObject = Get-NewOAuthToken @createOAuthTokenParams if ($oAuthReturnObject.Successful -eq $false) { Write-Host "" Write-Host "Unable to refresh EWS OAuth token. Please review the error message below and re-run the script:" -ForegroundColor Red Write-Host $oAuthReturnObject.ExceptionMessage -ForegroundColor Red exit } $Token.Value = $oAuthReturnObject.OAuthToken $script:tokenLastRefreshTime = $oAuthReturnObject.LastTokenRefreshTime $EWSService.Value = EWSAuth -Environment $Environment -Token $Token.Value -EWSOnlineURL $EWSOnlineURL } } ## function to get our named search folder function GetSearchFolder { param($ewsService, $userMailbox) $searchFoldersId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root, $userMailbox) $searchFoldersFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($ewsService, $searchFoldersId) $searchFoldersView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(1) $searchFoldersFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName, $searchFolderName) $results = $searchFoldersFolder.FindFolders($searchFoldersFilter, $searchFoldersView) return $results } ## function to create our named search folder function NewSearchFolder { param($ewsService, $userMailbox) $rootFolderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $userMailbox) $searchFoldersId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Root, $userMailbox) $mySearchFolder = New-Object Microsoft.Exchange.WebServices.Data.SearchFolder($ewsService) $mySearchFolder.DisplayName = $searchFolderName $mySearchFolder.SearchParameters.SearchFilter = $searchFilterCollection $mySearchFolder.SearchParameters.RootFolderIds.Add($rootFolderId) $mySearchFolder.SearchParameters.Traversal = [Microsoft.Exchange.WebServices.Data.SearchFolderTraversal]::Deep [void]$mySearchFolder.Save($searchFoldersId) } function RemoveSearchFolder { param($ewsService, $userMailbox) $searchFolders = @(GetSearchFolder $ewsService $userMailbox) if ($searchFolders.Count -gt 0) { Write-Host " Cleaning up search folder." [void]$searchFolders[0].Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) } } function GetSearchFolderItemResults { param($ewsService, $userMailbox) $searchFolders = @(GetSearchFolder $ewsService $userMailbox) if ($searchFolders.Count -lt 1) { Write-Warning "Search folder missing. Could not get results." return } $offset = 0 $pageSize = 100 $findItemsResults = $null do { $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView($pageSize, $offset) $findItemsResults = $searchFolders[0].FindItems($itemView) foreach ($item in $findItemsResults.Items) { $item } $offset = $findItemsResults.NextPageOffset } while ($findItemsResults.MoreAvailable) } $mailAddresses = New-Object System.Collections.ArrayList } process { foreach ($address in $UserMailboxes) { [void]$mailAddresses.Add($address) } } end { Write-Host ("CVE-2023-23397 script version $($BuildVersion)") -ForegroundColor Green Write-Verbose "PowerShell version: $($PSVersionTable.PSVersion)" # Using either of these switches implies -UseSearchFolders if ($SearchFolderCleanup -or $SkipSearchFolderCreation) { # But using them together is not valid if ($SearchFolderCleanup -and $SkipSearchFolderCreation) { Write-Host "The -SearchFolderCleanup and -SkipSearchFolderCreation switches cannot be used together." exit } $UseSearchFolders = $true } if (([System.String]::IsNullOrEmpty($CleanupInfoFilePath)) -and ($ScriptUpdateOnly -eq $false) -and ($UseSearchFolders -eq $false) -and ($SearchFolderCleanup -eq $false)) { $newSearchFeatureWording = "Did you know?" + "`nThe new asynchronous search feature is now general available (GA)!" + "`nWe recommend using it because the search performance is significantly increased." + "`nYou can enable it by using the the following parameters:" + "`n" + "`nUseSearchFolders: Enable deep-traversal SearchFolders search to significantly improve performance" + "`nSearchFolderCleanup: Clean up any SearchFolders left behind by the -UseSearchFolders switch" + "`n" + "`nThis version of the script also supports Certificate Based Authentication (CBA)" + "`n" + "`nMore information: https://aka.ms/CVE-2023-23397ScriptDocFAQ" Write-Host "" Write-Host $newSearchFeatureWording -ForegroundColor Cyan Write-Host "" Start-Sleep -Seconds 3 } if ($ScriptUpdateOnly) { switch (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/CVE-2023-23397-VersionsUrl" -Confirm:$false) { ($true) { Write-Host ("Script was successfully updated") -ForegroundColor Green } ($false) { Write-Host ("No update of the script performed") -ForegroundColor Yellow } default { Write-Host ("Unable to perform ScriptUpdateOnly operation") -ForegroundColor Red } } return } if ((-not($SkipVersionCheck)) -and (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/CVE-2023-23397-VersionsUrl" -Confirm:$false)) { Write-Host ("Script was updated. Please rerun the command") -ForegroundColor Yellow return } if ($IgnoreCertificateMismatch) { Write-Verbose ("IgnoreCertificateMismatch was used -policy will be set to: TrustAllCertsPolicy") Enable-TrustAnyCertificateCallback } if ($CreateAzureApplication) { $graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint if ($null -eq $graphAccessToken) { Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red exit } $createAzureApplicationParams = @{ AzAccountsObject = $graphAccessToken AzureApplicationName = $AzureApplicationName GraphApiUrl = $graphApiEndpoint } $return = New-EwsAzureApplication @createAzureApplicationParams if ($null -ne $return.AppId) { Write-Host "The application was successfully created" -ForegroundColor Green } else { Write-Host "Something went wrong while creating the application" -ForegroundColor Red } exit } if ($DeleteAzureApplication) { $graphAccessToken = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint if ($null -eq $graphAccessToken) { Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red exit } $deleteAzureApplicationParams = @{ AzAccountsObject = $graphAccessToken AzureApplicationName = $AzureApplicationName GraphApiUrl = $graphApiEndpoint } Remove-AzureApplication @deleteAzureApplicationParams exit } $path = $DLLPath if ([System.String]::IsNullOrEmpty($path)) { Write-Host "Trying to find Microsoft.Exchange.WebServices.dll in the script folder" $path = (Get-ChildItem -LiteralPath "$PSScriptRoot\EWS" -Recurse -Filter "Microsoft.Exchange.WebServices.dll" -ErrorAction SilentlyContinue | Select-Object -First 1).FullName if ([System.String]::IsNullOrEmpty($path)) { Write-Host "Microsoft.Exchange.WebServices.dll wasn't found - attempting to download it from the internet" -ForegroundColor Yellow $nuGetPackage = Get-NuGetPackage -PackageId "Microsoft.Exchange.WebServices" -Author "Microsoft" if ($nuGetPackage.DownloadSuccessful) { $unzipNuGetPackage = Invoke-ExtractArchive -CompressedFilePath $nuGetPackage.NuGetPackageFullPath -TargetFolder "$PSScriptRoot\EWS" if ($unzipNuGetPackage.DecompressionSuccessful) { $path = (Get-ChildItem -Path $unzipNuGetPackage.FullPathToDecompressedFiles -Recurse -Filter "Microsoft.Exchange.WebServices.dll" | Select-Object -First 1).FullName } else { Write-Host "Failed to unzip Microsoft.Exchange.WebServices.dll. Please unzip the package manually." -ForegroundColor Red exit } } else { Write-Host "Failed to download Microsoft.Exchange.WebServices.dll from the internet. Please download the package manually and extract the dll. Provide the path to dll using DLLPath parameter." -ForegroundColor Red exit } } else { Write-Host "Microsoft.Exchange.WebServices.dll was found in the script folder" -ForegroundColor Green } } try { Import-Module -Name $path -ErrorAction Stop } catch { Write-Host "Failed to import Microsoft.Exchange.WebServices.dll Inner Exception`n`n$_" -ForegroundColor Red exit } $failedMailboxes = New-Object 'System.Collections.Generic.List[string]' $invalidEntries = New-Object 'System.Collections.Generic.List[string]' $removedEntries = New-Object 'System.Collections.Generic.List[string]' #MailInfo $mailInfo = @{ "Id" = [Microsoft.Exchange.WebServices.Data.ItemSchema]::Id "Sender" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::From "Recipient" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::ToRecipients "Subject" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject "DateReceived" = [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::DateTimeReceived "PidLidReminderFileParameter" = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition([Microsoft.Exchange.WebServices.Data.DefaultExtendedPropertySet]::Common, 0x0000851F, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::String) "ItemClass" = [Microsoft.Exchange.WebServices.Data.ItemSchema]::ItemClass } if ($Environment -eq "Online") { if (([System.String]::IsNullOrEmpty($AppId)) -or ([System.String]::IsNullOrEmpty($Organization)) -or ([System.String]::IsNullOrEmpty($CertificateThumbprint))) { # We need to query the Azure application information from the Azure AD if not explicitly provided via parameter $azAccountsObject = Get-GraphAccessToken -AzureADEndpoint $azureADEndpoint -GraphApiUrl $graphApiEndpoint if ($null -eq $azAccountsObject) { Write-Host "Failed to acquire an access token - the script cannot continue" -ForegroundColor Red exit } $application = Get-AzureApplication -AzAccountsObject $azAccountsObject -AzureApplicationName $AzureApplicationName -GraphApiUrl $graphApiEndpoint $tenantID = $azAccountsObject.TenantId $clientID = $application.AppId } else { $tenantID = $Organization $clientID = $AppId } $applicationInfo = @{ "TenantID" = $tenantID "ClientID" = $clientID } if ([System.String]::IsNullOrEmpty($CertificateThumbprint)) { $secret = New-AzureApplicationAppSecret -AzAccountsObject $azAccountsObject -AzureApplicationName $AzureApplicationName -GraphApiUrl $graphApiEndpoint if ([System.String]::IsNullOrEmpty($secret)) { Write-Host "Unable to generate application secret for Azure application: $AzureApplicationName" -ForegroundColor Red exit } $applicationInfo.Add("AppSecret", $secret) } else { $jwtParams = @{ CertificateThumbprint = $CertificateThumbprint CertificateStore = "CurrentUser" Issuer = $clientID Audience = "$azureADEndpoint/$tenantID/oauth2/v2.0/token" Subject = $clientID } $jwt = Get-NewJsonWebToken @jwtParams if ($null -eq $jwt) { Write-Host "Unable to generate Json Web Token by using certificate: $CertificateThumbprint" -ForegroundColor Red exit } $applicationInfo.Add("AppSecret", $jwt) $applicationInfo.Add("CertificateThumbprint", $CertificateThumbprint) } $createOAuthTokenParams = @{ TenantID = $tenantID ClientID = $clientID Secret = $applicationInfo.AppSecret Scope = $ewsOnlineScope Endpoint = $azureADEndpoint CertificateBasedAuthentication = (-not([System.String]::IsNullOrEmpty($CertificateThumbprint))) } #Create OAUTH token $oAuthReturnObject = Get-NewOAuthToken @createOAuthTokenParams if ($oAuthReturnObject.Successful -eq $false) { Write-Host "" Write-Host "Unable to fetch an OAuth token for accessing EWS. Please review the error message below and re-run the script:" -ForegroundColor Red Write-Host $oAuthReturnObject.ExceptionMessage -ForegroundColor Red exit } $EWSToken = $oAuthReturnObject.OAuthToken $script:tokenLastRefreshTime = $oAuthReturnObject.LastTokenRefreshTime $ewsService = EWSAuth -Environment $Environment -Token $EWSToken -EWSOnlineURL $ewsOnlineURL } else { #Server $EWSToken = $null $ewsService = EWSAuth -Environment $Environment -EWSServerURL $EWSServerURL } if ($PSCmdlet.ParameterSetName -eq "Audit") { if ($null -eq $mailAddresses -or $mailAddresses.Count -eq 0) { Write-Host "No mailbox provided" -ForegroundColor Red exit } $csvFileName = ("AuditResults_$(Get-Date -Format "yyyyMMdd_HHmmss").csv") $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView([int]::MaxValue) $searchFilterCollection = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection if ($null -ne $StartTimeFilter -and -not $UseSearchFolders) { $searchFilterStartTime = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsGreaterThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeCreated, $StartTimeFilter) $searchFilterCollection.Add($searchFilterStartTime) } if ($null -ne $EndTimeFilter -and -not $UseSearchFolders) { $searchFilterEndTime = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsLessThan([Microsoft.Exchange.WebServices.Data.ItemSchema]::DateTimeCreated, $EndTimeFilter) $searchFilterCollection.Add($searchFilterEndTime) } $searchFilterPidLidReminderFileParameterExists = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+Exists($mailInfo["PidLidReminderFileParameter"]) $searchFilterCollection.Add($searchFilterPidLidReminderFileParameterExists) $PropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet foreach ($key in $mailInfo.Keys) { $PropertySet.Add($mailInfo[$key]) } $mailboxProcessed = 0 $rowCount = 0 if ($UseSearchFolders) { $actionText = "Creating" if ($SearchFolderCleanup) { $actionText = "Removing" } $searchFolderCreationTimer = $null if (-not $SkipSearchFolderCreation) { foreach ($mailAddress in $mailAddresses) { $ewsService.HttpHeaders.Clear() $ewsService.HttpHeaders.Add("X-AnchorMailbox", $mailAddress) Write-Host ("$actionText search folders in $($mailboxProcessed + 1) of $($mailAddresses.Count) mailboxes (currently: $mailAddress)") $userMailbox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($mailAddress) if ($null -eq $userMailbox) { Write-Host ("Unable to get mailbox associated with mail address $mailAddress") $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) if ($SearchFolderCleanup) { RemoveSearchFolder $ewsService $userMailbox $mailboxProcessed += 1 continue } $searchFolders = GetSearchFolder $ewsService $userMailbox if ($searchFolders.Count -gt 0) { Write-Host " Search folder already exists in this mailbox." $mailboxProcessed += 1 continue } # Create a new search folder if ($null -eq $searchFolderCreationTimer) { $searchFolderCreationTimer = New-Object System.Diagnostics.Stopwatch $searchFolderCreationTimer.Start() } NewSearchFolder $ewsService $userMailbox $mailboxProcessed += 1 } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { Write-Host ("Unable to access mailbox: $mailAddress") -ForegroundColor Red Write-Host ("Inner Exception: $_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } catch { Write-Host ("Unable to process mailbox $mailAddress as it seems to be inaccessible. Inner Exception:`n`n$_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } } } if (-not $SearchFolderCleanup) { if ($null -ne $SearchFolderCreationTimer) { $waitSecondsAfterCreatingFirstSearchFolder = 60 if ($SearchFolderCreationTimer.ElapsedMilliseconds -lt $waitSecondsAfterCreatingFirstSearchFolder * 1000) { $secondsElapsed = $SearchFolderCreationTimer.ElapsedMilliseconds / 1000 $secondsRemaining = $waitSecondsAfterCreatingFirstSearchFolder - $secondsElapsed Write-Host "First search folder created $secondsElapsed seconds ago. Waiting $secondsRemaining seconds to allow for population." Start-Sleep -Seconds $secondsRemaining } } $mailboxProcessed = 0 foreach ($mailAddress in $mailAddresses) { if ($failedMailboxes.Contains($mailAddress)) { $mailboxProcessed += 1 continue } $ewsService.HttpHeaders.Clear() $ewsService.HttpHeaders.Add("X-AnchorMailbox", $mailAddress) Write-Host ("Getting search results in $($mailboxProcessed + 1) of $($mailAddresses.Count) mailboxes (currently: $mailAddress)") $userMailbox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($mailAddress) $itemResults = @() try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) [Microsoft.Exchange.WebServices.Data.Item[]]$itemResults = @(GetSearchFolderItemResults $ewsService $userMailbox) } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { Write-Host ("Unable to access mailbox: $mailAddress") -ForegroundColor Red Write-Host ("Inner Exception: $_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } catch { Write-Host ("Unable to process mailbox $mailAddress as it seems to be inaccessible. Inner Exception:`n`n$_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } $IdsProcessed = New-Object 'System.Collections.Generic.HashSet[string]' if ($itemResults.Count -lt 1) { $mailboxProcessed += 1 continue } $items = $ewsService.LoadPropertiesForItems($itemResults, $PropertySet) foreach ($item in $items) { if ($null -ne $StartTimeFilter -and $item.Item.DateTimeReceived -lt $StartTimeFilter) { continue } if ($null -ne $EndTimeFilter -and $item.Item.DateTimeReceived -gt $EndTimeFilter) { continue } if (-not $IdsProcessed.Contains($item.Item.Id) -and -not [System.String]::IsNullOrEmpty($item.Item.ExtendedProperties[0].Value)) { CreateCustomCSV -Mailbox $mailAddress -Data $item.Item -CsvPath $csvFileName $rowCount ++ if ($rowCount -ge $MaxCSVLength) { Write-Host ("The csv file has reached it's maximum limit of $MaxCSVLength rows... aborting... Please apply appropriate filters to reduce the result size") Write-Host ("Please find the audit results in $csvFileName created in the current folder.") exit } [void]$IdsProcessed.Add($item.Item.Id) } } $mailboxProcessed ++ } } } else { foreach ($mailAddress in $mailAddresses) { $ewsService.HttpHeaders.Clear() $ewsService.HttpHeaders.Add("X-AnchorMailbox", $mailAddress) Write-Host ("Scanning $($mailboxProcessed + 1) of $($mailAddresses.Count) mailboxes (currently: $mailAddress)") $userMailbox = New-Object Microsoft.Exchange.WebServices.Data.Mailbox($mailAddress) if ($null -eq $userMailbox) { Write-Host ("Unable to get mailbox associated with mail address $mailAddress") $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) $rootFolderId = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot, $userMailbox) $rootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($ewsService, $rootFolderId) # Create a new ArrayList to hold the folder $foldersList = New-Object System.Collections.ArrayList GetSubFolders -Folder $rootFolder -FoldersList $foldersList } catch [Microsoft.Exchange.WebServices.Data.ServiceResponseException] { Write-Host ("Unable to access mailbox: $mailAddress") -ForegroundColor Red Write-Host ("Inner Exception: $_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } catch { Write-Host ("Unable to process mailbox $mailAddress as it seems to be inaccessible. Inner Exception:`n`n$_") -ForegroundColor Red $failedMailboxes.Add($mailAddress) $mailboxProcessed += 1 continue } $IdsProcessed = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($folder in $foldersList) { try { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $mailAddress) $results = $ewsService.FindItems($folder.Id, $searchFilterCollection, $itemView) if ($null -ne $results -and $null -ne $results.Items -and $results.Items.Count -gt 0) { $items = $ewsService.LoadPropertiesForItems($results.Items, $PropertySet) } else { continue } foreach ($item in $items) { if (-not $IdsProcessed.Contains($item.Item.Id) -and -not [System.String]::IsNullOrEmpty($item.Item.ExtendedProperties[0].Value)) { CreateCustomCSV -Mailbox $mailAddress -Data $item.Item -CsvPath $csvFileName $rowCount ++ if ($rowCount -ge $MaxCSVLength) { Write-Host ("The csv file has reached it's maximum limit of $MaxCSVLength rows... aborting... Please apply appropriate filters to reduce the result size") Write-Host ("Please find the audit results in $csvFileName created in the current folder.") exit } [void]$IdsProcessed.Add($item.Item.Id) } } } catch { Write-Host "Error while scanning $($folder.DisplayName) of the mailbox $mailAddress. Inner Exception:`n`n$_" -ForegroundColor Red } } $mailboxProcessed ++ } } if (-not $SearchFolderCleanup) { if ($rowCount -eq 0) { Write-Host "No vulnerable item found" -ForegroundColor Green } else { Write-Host ("Please find the audit results in $csvFileName created in the current folder.") } } } else { $params = @{ Message = "Display Warning about Store operation" Target = "The script will perform store operation on mailboxes using EWS" Operation = "" } if ($CleanupAction -eq "ClearProperty") { $params.Operation = "Clear the PidLidReminderFileParameter property of mail items" } if ($CleanupAction -eq "ClearItem") { $params.Operation = "Delete items" } Show-Disclaimer @params $importedCsvData = Import-Csv $CleanupInfoFilePath -Delimiter "," if ($null -ne $importedCsvData) { $headerLine = ($importedCsvData[0] | Get-Member | Where-Object { ($_.MemberType -eq "NoteProperty") }).Name if ($headerLine.Count -eq 1) { # Csv file which isn't in the expected format has only 1 NoteProperty after being imported Write-Host "Csv file isn't in the expected format and must be normalized" try { # Process line by line and pipe to ConvertFrom-Csv to convert data to PSObject type $cleanupCSV = $importedCsvData | ForEach-Object { ($_.$headerLine) } | ConvertFrom-Csv -Header ((($headerLine).Replace('"', "")).Split(",")) -ErrorAction Stop } catch { Write-Host "Something went wrong while normalizing the csv file. Inner Exception:`n`n$_" -ForegroundColor Red exit } } elseif ($headerLine.Count -eq 9) { # Csv file which is in the expected format has 9 NoteProperties after being imported Write-Host "Csv file is in the expected format and can be used for cleanup" $cleanupCSV = $importedCsvData } else { # Everything else is unexpected and we stop processing here Write-Host "Unable to normalize the csv file please reach out to Microsoft support" -ForegroundColor Red exit } } else { Write-Host "Something went wrong while importing the csv file" -ForegroundColor Red exit } $entryCount = 0 foreach ($entry in $cleanupCSV) { $entryCount ++ if ([System.String]::IsNullOrEmpty($entry.Id)) { Write-Host ("No Id present for entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } if ([System.String]::IsNullOrEmpty($entry.Mailbox)) { Write-Host ("No Mailbox address present for entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } if ($null -ne $entry.Cleanup -and $entry.Cleanup.ToLower() -eq "y") { # Check for token expiry CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $entry.Mailbox) $item = FindItem -ExchangeService $ewsService -Id $entry.Id if ($null -ne $item) { try { if ($CleanupAction -eq "ClearItem") { $item.Delete([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete) } else { if (-not $item.RemoveExtendedProperty($mailInfo["PidLidReminderFileParameter"])) { Write-Host ("Property already cleared on entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Yellow $invalidEntries.Add($entryCount) continue } CheckTokenExpiry -Environment $Environment -Token ([ref]$EWSToken) -EWSService ([ref]$ewsService) -ApplicationInfo $applicationInfo -EWSOnlineURL $ewsOnlineURL -EWSOnlineScope $ewsOnlineScope -AzureADEndpoint $azureADEndpoint $ewsService.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $entry.Mailbox) if ($entry.ItemType -eq "Calendar") { $item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite, [Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone) } else { $item.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite) } } # If we get here, it should be successful. $removedEntries.Add($entryCount) } catch { Write-Host ("Unable to perform cleanup action on entry number: $entryCount, Line number: $($entryCount + 1) Inner Exception`n`n$_") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } } else { Write-Host ("Unable to find item associated to entry number: $entryCount, Line number: $($entryCount + 1)") -ForegroundColor Red $invalidEntries.Add($entryCount) continue } } } Write-Host "Successfully removed $($removedEntries.Count) of $($cleanupCSV.Count)" if ($removedEntries.Count -eq 0) { Write-Host "No entries were removed. Please update the Cleanup column for the items you wish to cleanup." -ForegroundColor Yellow } else { Write-Host "Completed cleanup operation!" } } if ($mode -eq "Audit" -and $null -ne $failedMailboxes -and $failedMailboxes.Count -gt 0) { Write-Host ("Couldn't Audit mailboxes: {0}" -f [string]::Join(", ", $failedMailboxes)) } if ($mode -eq "Cleanup" -and $null -ne $invalidEntries -and $invalidEntries.Count -gt 0) { Write-Host ("Couldn't Cleanup the entries: {0}" -f [string]::Join(", ", $invalidEntries)) } Write-Host "" Write-Host ("Do you have feedback regarding the script? Please email ExToolsFeedback@microsoft.com.") -ForegroundColor Green Write-Host "" Remove-Module -Name "Microsoft.Exchange.WebServices" -ErrorAction SilentlyContinue }