Delete specific emails from Office 365 inboxes in customer tenants via PowerShell

Check Office 365 account email against Have I been Pwned

There are plenty of scenarios where it can be handy to retrieve and delete specific emails from user mailboxes. And while Microsoft provides an excellent method to do this, it doesn’t address every scenario.

This script is designed for Microsoft Partners or Office 365 admins who need to quickly script or automate the removal of certain emails from their own and delegated customers’ user mailboxes without having to assign specific roles.

Please note that this script should be used with a high degree of caution and consideration.

Microsoft Partners can use this script to retrieve and delete emails that match a certain criteria from all user mailboxes in all customer tenants. For instance, if a bad actor is sending a targeted phishing email to multiple customer tenants, you can use this script to retrieve and delete the messages from all customer tenants at once.

  • Step 1 – Create the application with permission to access customer tenants mailboxes
  • Step 2 – Edit the script to select the emails that you want to remove, export them to a CSV, and confirm removal.

Create an Azure AD Application with permission to access emails in your own and your customers’ tenants

  1. Double click the below script to select it.
  2. Copy and paste the script into a new file in Visual Studio Code and save it with a .ps1 extension
  3. Install the recommended PowerShell module if you haven’t already
  4. Modify the $homePage and $logoutURI values to any valid URIs that you like. They don’t need to be actual addresses, so feel free to make something up. Set the $appIDUri variable to a use a valid domain in your tenant. eg. https://yourdomain.com/$((New-Guid).ToString())Update HomePage and AppIDUri
  5. If you are running the script for a single tenant, comment out the following two lines:Run Script For A Single Tenant
  6. Press F5 to run the script
  7. Sign in to Azure AD using your global admin credentials. Note that the login window may appear behind Visual Studio Code. The following screenshots are examples, so don’t worry if the Permissions/App names in the screenshot don’t match up. Create Azure AD Application To Access Microsoft Graph Security Alerts
  8. Wait for the script to complete. Azure AD Application Adding Permissions
  9. Retrieve the client ID, client secret and tenant ID from the exported CSV at C:\temp\azureadapp.csv. (below image is just an example.)Exported Info for Azure Ad App

PowerShell Script to create and authorise an application to access mailboxes in customer tenants

# This script needs to be run by an admin account in your Office 365 tenant
# This script will create an Azure AD app in your organisation with permission
# to access resources in yours and your customers' tenants.
# It will export information about the application to a CSV located at C:\temp\.
# The CSV will include the Client ID and Secret of the application, so keep it safe.
   
# Confirm C:\temp exists
$temp = Test-Path -Path C:\temp
if ($temp) {
    #Write-Host "Path exists"
}
else {
    Write-Host "Creating Temp folder"
    New-Item -Path C:\temp -ItemType directory
}
   
$applicationName = "GCITS Mailbox Worker"
   
# Change this to true if you would like to overwrite any existing applications with matching names. 
$removeExistingAppWithSameName = $false
# Modify the homePage, appIdURI and logoutURI values to whatever valid URI you like. 
# They don't need to be actual addresses, so feel free to make something up.
$homePage = "https://secure.gcits.com"
$appIdURI = "https://secure.gcits.com/$((New-Guid).ToString())"
$logoutURI = "https://portal.office.com"
   
$URIForApplicationPermissionCall = "https://graph.microsoft.com/v1.0/users"
$ApplicationPermissions = "Directory.Read.All MailboxSettings.ReadWrite Mail.ReadWrite"
   
Function Add-ResourcePermission($requiredAccess, $exposedPermissions, $requiredAccesses, $permissionType) {
    foreach ($permission in $requiredAccesses.Trim().Split(" ")) {
        $reqPermission = $null
        $reqPermission = $exposedPermissions | Where-Object { $_.Value -contains $permission }
        Write-Host "Collected information for $($reqPermission.Value) of type $permissionType" -ForegroundColor Green
        $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess
        $resourceAccess.Type = $permissionType
        $resourceAccess.Id = $reqPermission.Id    
        $requiredAccess.ResourceAccess.Add($resourceAccess)
    }
}
   
Function Get-RequiredPermissions($requiredDelegatedPermissions, $requiredApplicationPermissions, $reqsp) {
    $sp = $reqsp
    $appid = $sp.AppId
    $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess
    $requiredAccess.ResourceAppId = $appid
    $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess]
    if ($requiredDelegatedPermissions) {
        Add-ResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope"
    } 
    if ($requiredApplicationPermissions) {
        Add-ResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role"
    }
    return $requiredAccess
}
Function New-AppKey ($fromDate, $durationInYears, $pw) {
    $endDate = $fromDate.AddYears($durationInYears) 
    $keyId = (New-Guid).ToString()
    $key = New-Object Microsoft.Open.AzureAD.Model.PasswordCredential($null, $endDate, $keyId, $fromDate, $pw)
    return $key
}
   
Function Test-AppKey($fromDate, $durationInYears, $pw) {
   
    $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw
    while ($testKey.Value -match "\+" -or $testKey.Value -match "/") {
        Write-Host "Secret contains + or / and may not authenticate correctly. Regenerating..." -ForegroundColor Yellow
        $pw = Initialize-AppKey
        $testKey = New-AppKey -fromDate $fromDate -durationInYears $durationInYears -pw $pw
    }
    Write-Host "Secret doesn't contain + or /. Continuing..." -ForegroundColor Green
    $key = $testKey
   
    return $key
}
   
Function Initialize-AppKey {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"
    $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    $aesManaged.GenerateKey()
    return [System.Convert]::ToBase64String($aesManaged.Key)
}
function Confirm-MicrosoftGraphServicePrincipal {
    $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph"
    if (!$graphsp) {
        $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft.Azure.AgregatorService"
    }
    if (!$graphsp) {
        Login-AzureRmAccount -Credential $credentials
        New-AzureRmADServicePrincipal -ApplicationId "00000003-0000-0000-c000-000000000000"
        $graphsp = Get-AzureADServicePrincipal -SearchString "Microsoft Graph"
    }
    return $graphsp
}
Write-Host "Connecting to Azure AD. The login window may appear behind Visual Studio Code."
Connect-AzureAD
   
Write-Host "Creating application in tenant: $((Get-AzureADTenantDetail).displayName)"
   
# Check for the Microsoft Graph Service Principal. If it doesn't exist already, create it.
$graphsp = Confirm-MicrosoftGraphServicePrincipal
   
$existingapp = $null
$existingapp = Get-AzureADApplication -SearchString $applicationName
if ($existingapp -and $removeExistingAppWithSameName) {
    Remove-AzureADApplication -ObjectId $existingApp.objectId
}
   
# RSPS 
$rsps = @()
if ($graphsp) {
    $rsps += $graphsp
    $tenant_id = (Get-AzureADTenantDetail).ObjectId
    $tenantName = (Get-AzureADTenantDetail).DisplayName
   
    # Add Required Resources Access (Microsoft Graph)
    $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]
    $microsoftGraphRequiredPermissions = Get-RequiredPermissions -reqsp $graphsp -requiredApplicationPermissions $ApplicationPermissions -requiredDelegatedPermissions $DelegatedPermissions
    $requiredResourcesAccess.Add($microsoftGraphRequiredPermissions)
   
    # Get an application key
    $pw = Initialize-AppKey
    $fromDate = [System.DateTime]::Now
    $appKey = Test-AppKey -fromDate $fromDate -durationInYears 99 -pw $pw
   
    Write-Host "Creating the AAD application $applicationName" -ForegroundColor Blue
    $aadApplication = New-AzureADApplication -DisplayName $applicationName `
        -HomePage $homePage `
        -ReplyUrls $homePage `
        -IdentifierUris $appIdURI `
        -LogoutUrl $logoutURI `
        -RequiredResourceAccess $requiredResourcesAccess `
        -PasswordCredentials $appKey `
        -AvailableToOtherTenants $true
       
    # Creating the Service Principal for the application
    $servicePrincipal = New-AzureADServicePrincipal -AppId $aadApplication.AppId
   
    Write-Host "Assigning Permissions" -ForegroundColor Yellow
     
    # Assign application permissions to the application
    foreach ($app in $requiredResourcesAccess) {
        $reqAppSP = $rsps | Where-Object { $_.appid -contains $app.ResourceAppId }
        Write-Host "Assigning Application permissions for $($reqAppSP.displayName)" -ForegroundColor DarkYellow
        foreach ($resource in $app.ResourceAccess) {
            if ($resource.Type -match "Role") {
                New-AzureADServiceAppRoleAssignment -ObjectId $serviceprincipal.ObjectId `
                    -PrincipalId $serviceprincipal.ObjectId -ResourceId $reqAppSP.ObjectId -Id $resource.Id
            }
        }
    }
     
    # This provides the application with access to your customer tenants.
    $group = Get-AzureADGroup -Filter "displayName eq 'Adminagents'"
    Add-AzureADGroupMember -ObjectId $group.ObjectId -RefObjectId $servicePrincipal.ObjectId
 
    Write-Host "App Created" -ForegroundColor Green
     
    # Define parameters for Microsoft Graph access token retrieval
    $client_id = $aadApplication.AppId;
    $client_secret = $appkey.Value
    $tenant_id = (Get-AzureADTenantDetail).ObjectId
    $resource = "https://graph.microsoft.com"
    $authority = "https://login.microsoftonline.com/$tenant_id"
    $tokenEndpointUri = "$authority/oauth2/token"
   
    # Get the access token using grant type password for Delegated Permissions or grant type client_credentials for Application Permissions
   
    $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource"
   
    # Try to execute the API call 6 times
   
    $Stoploop = $false
    [int]$Retrycount = "0"
    do {
        try {
            $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing
            Write-Host "Retrieved Access Token" -ForegroundColor Green
            # Assign access token
            $access_token = $response.access_token
            $body = $null
   
            $body = Invoke-RestMethod `
                -Uri $UriForApplicationPermissionCall `
                -Headers @{"Authorization" = "Bearer $access_token" } `
                -ContentType "application/json" `
                -Method GET  `
   
            Write-Host "Retrieved Graph content" -ForegroundColor Green
            $Stoploop = $true
        }
        catch {
            if ($Retrycount -gt 5) {
                Write-Host "Could not get Graph content after 6 retries." -ForegroundColor Red
                $Stoploop = $true
            }
            else {
                Write-Host "Could not get Graph content. Retrying in 5 seconds..." -ForegroundColor DarkYellow
                Start-Sleep -Seconds 5
                $Retrycount ++
            }
        }
    }
    While ($Stoploop -eq $false)
   
    $appInfo = [pscustomobject][ordered]@{
        ApplicationName        = $ApplicationName
        TenantName             = $tenantName
        TenantId               = $tenant_id
        clientId               = $client_id
        clientSecret           = $client_secret
        ApplicationPermissions = $ApplicationPermissions
    }
       
    $AppInfo | Export-Csv C:\temp\AzureADApp.csv -Append -NoTypeInformation
}
else {
    Write-Host "Microsoft Graph Service Principal could not be found or created" -ForegroundColor Red
}

Removing specific emails in Office 365 via PowerShell

Preparing the Office 365 email deletion PowerShell script

  1. Copy and paste the below script into Visual Studio Code and save it with a .ps1 extension.
  2. Retrieve the Tenant ID, Client ID and Client Secret from the C:\temp\AzureADApp.csv file created by the previous script. Paste these values into the $ourTenantId, $clientId and $clientSecret values at the top of the second script.

Define the queries for the emails you would like to delete

In this case, we are retrieving emails sent to and from a specific address using filter and search queries located on lines 79 and 80 of the script. However you can use the odata queries on the Microsoft Graph to filter on whatever you like. See here for more information: https://docs.microsoft.com/en-us/graph/query-parameters

The filter query for emails sent from a particular address looks like this:

"users/$($user.userPrincipalName)/messages?`$filter=from/emailAddress/address+eq+'user@domain.com'"

The search query for emails sent to a particular address looks like this:

"users/$($user.userPrincipalName)/messages?`$search=`"recipients:user@domain.com`""

Before running the script, be sure to update the queries to retrieve the emails that you want to delete.

Note that while we are performing two queries in separate calls in the example, you can combine multiple queries in a single call to save time.

  1. When you’re happy with your queries, press F5 to run the script
  2. If you’re a Microsoft Partner with delegated access to customer tenants, you can select the tenant or tenants that you would like to search. Note that the grid view window may appear behind Visual Studio Code.Choose Office 365 Tenants To Search For Specific Emails PowerShell
  3. The script will loop through the specified tenants and user mailboxes and search for emails matching your criteria. Some users may not have mailboxes and will display a ‘couldn’t access message’.Searching For Emails In Office 365 Mailboxes Matching Criteria
  4. Once the script has completed searching your mailboxes, it will export a CSV containing a list of emails to C:\temp\pendingDeletion.csvCSV of Office 365 Emails Pending Deletion By PowerShell
  5. You can review the CSV to confirm that you would like to delete the specified emails.
  6. After reviewing the CSV and removing any entries that you don’t want to delete, enter ‘y’ to confirm deletion and wait for the script to complete.Deleting Emails Via PowerShell Office 365

PowerShell script to delete specific emails from Office 365 tenants

$clientId = "Enter_Client_ID_Here"
$clientSecret = "Enter_Client_Secret_Here"
$ourTenantId = "Enter_Your_Tenant_ID_Here"

function Get-GCITSAccessToken($appCredential, $tenantId) {
    $client_id = $appCredential.appID
    $client_secret = $appCredential.secret
    $tenant_id = $tenantid
    $resource = "https://graph.microsoft.com"
    $authority = "https://login.microsoftonline.com/$tenant_id"
    $tokenEndpointUri = "$authority/oauth2/token"
    $content = "grant_type=client_credentials&client_id=$client_id&client_secret=$client_secret&resource=$resource"
    $response = Invoke-RestMethod -Uri $tokenEndpointUri -Body $content -Method Post -UseBasicParsing
    $access_token = $response.access_token
    return $access_token
}

function Get-GCITSMSGraphResource($Resource) {
    $graphBaseUri = "https://graph.microsoft.com/v1.0"
    $values = @()
    $result = Invoke-RestMethod -Uri "$graphBaseUri/$resource" -Headers $headers
    if ($result.value) {
        $values += $result.value
        if ($result."@odata.nextLink") {
            do {
                $result = Invoke-RestMethod -Uri $result."@odata.nextLink" -Headers $headers
                $values += $result.value
            } while ($result."@odata.nextLink")
        }
    }
    else {
        $values = $result
    }
    return $values
}

$appCredential = @{
    AppId  = $clientId
    Secret = $clientSecret
}


$accessToken = Get-GCITSAccessToken -appCredential $appCredential -tenantId $ourTenantId
$headers = @{
    Authorization = "Bearer $accesstoken"
}

$ourTenantInfo = Get-GCITSMSGraphResource -Resource organization

$customers = @()
$customers += [pscustomobject][ordered]@{
    customerid        = $ourtenantid
    defaultDomainName = ($ourTenantInfo.verifiedDomains | Where-Object { $_.isinitial }).name
    displayName       = $ourTenantInfo.displayName
}
try {
    $customers += Get-GCITSMSGraphResource -Resource contracts
}
catch {
    Write-Host "Couldn't retrieve any customer tenants"
}

if (($customers | Measure-Object).count -gt 1) {
    $customers = $customers | Out-GridView -PassThru
}

$emailCollection = @()
foreach ($customer in $customers) {
    Write-Host "Searching $($customer.displayname)" -foregroundcolor blue
    $accessToken = Get-GCITSAccessToken -appCredential $appCredential -tenantId $customer.customerid
    $headers = @{
        Authorization = "Bearer $accessToken"
    }
    $users = Get-GCITSMSGraphResource -Resource users | Where-Object { $_.userPrincipalName -notmatch "#EXT#" }
    foreach ($user in $users) {
        $usersEmails = @()
        try {
            # Modify the below queries to retrieve emails that you would like to delete - eg emails sent and received from user@domain.com
            $usersEmails += Get-GCITSMSGraphResource -Resource "users/$($user.userPrincipalName)/messages?`$filter=from/emailAddress/address+eq+'user@domain.com'"
            $usersEmails += Get-GCITSMSGraphResource -Resource "users/$($user.userPrincipalName)/messages?`$search=`"recipients:user@domain.com`" "
            $count = ($usersEmails.id | Measure-Object).count
            if ($count -gt 0) {
                Write-Host "Searched mailbox: $($user.userPrincipalName). Found $count matching emails" -ForegroundColor green
            }
            else {
                Write-Host "Searched mailbox: $($user.userPrincipalName). No matches." -ForegroundColor darkgreen
            }
            
        }
        catch {
            Write-Host "Couldn't access: $($user.userprincipalname)" -ForegroundColor yellow
        }
        if ($usersEmails) {
            $usersEmails | Add-Member UserPrincipalName $user.userPrincipalName -Force
            $usersEmails | Add-Member TenantId $customer.customerId
            $usersEmails | Add-Member CustomerName $customer.displayName
            $emailCollection += $usersEmails
        }
    }
}

$emails = $emailCollection | Where-Object { $_.id }

$summary = $emails | Select-Object @{n = "CustomerName"; e = { $_.CustomerName } }, SentDateTime, `
@{n = "FromEmail"; e = { $_.from.emailAddress.address } }, @{n = "ToEmail"; e = { ($_.toRecipients.emailAddress | ForEach-Object { $_.address }) -join ", " } }, `
@{n = "ccEmail"; e = { ($_.ccRecipients.emailAddress | ForEach-Object { $_.address }) -join ", " } }, `
@{n = "bccEmail"; e = { ($_.bccRecipients.emailAddress | ForEach-Object { $_.address }) -join ", " } }, `
    Subject, @{n = "inMailbox"; e = { $_.userPrincipalName } }, id, @{n = "TenantId"; e = { $_.TenantId } }

$summary | Export-Csv C:\temp\PendingDeletion.csv -NoTypeInformation

$confirmDeletion = Read-Host "A list of emails for deletion has been exported to C:\temp\PendingDeletion. Please review and edit this CSV and confirm deletion by entering 'Y'"

if ($confirmDeletion.trim() -eq "y") {
    $toDelete = Import-Csv C:\temp\PendingDeletion.csv
    $groups = $toDelete | Group-Object TenantId
    foreach ($group in $groups) {
        Write-Host "Deleting emails from $($group.group.customername | Select-Object -First 1)"
        $accessToken = Get-GCITSAccessToken -appCredential $appCredential -tenantId $group.Name
        $headers = @{
            Authorization = "Bearer $accessToken"
        }
        foreach ($email in $group.group) {
            Write-Host "Deleting email from $($email.inmailbox) - Subject: $($email.subject)" -ForegroundColor yellow
            $delete = Invoke-RestMethod -Method Delete -Uri "https://graph.microsoft.com/v1.0/users/$($email.inMailbox)/messages/$($email.id)" -Headers $headers
        }
    }
}

Was this article helpful?

Related Articles