Back to script library
Entra / Microsoft 365 · Teams

Azure Automation: find and remove Teams chats

Runbook that searches Teams chat threads for a specific topic and removes matching conversations.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-MgGraph -NoWelcome

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

param(
[string] $TenantId = (Get-MgOrganization).Id,
[string] $AppId = "",
[string] $StartDate = (Get-Date).AddMonths(-1).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"),
[string] $EndDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
)
Function Add-MessageRecipients {
# Function to build an addressee list to send email
[cmdletbinding()]
Param(
[array]$ListOfAddresses )
ForEach ($SMTPAddress in $ListOfAddresses) {
@{
emailAddress = @{address = $SMTPAddress}
}
}
}
# Start
# Connect to the Graph SDK with the correct permissions - if run in Azure Automation, make sure that the automation account has these permissions.
# User.Read.All - read user accounts
# Chat.ReadWrite.All - read chat messages
# Chat.ManageDeletion.All - manage chat deletions
# Mail.Send - send the message
[array]$RequiredScopes = "User.Read.All", "Chat.ReadWrite.All", "Chat.ManageDeletion.All", "Mail.Send"
$Interactive = $false
# Determine if we're interactive or not
If ([Environment]::UserInteractive) {
# We're running interactively...
Clear-Host
Write-Host "Script running interactively... connecting to the Graph" -ForegroundColor Yellow
Connect-MgGraph -NoWelcome
$Interactive = $true
# Email address to use when sending email from interactive session
$MsgFrom = (Get-MgContext).Account
} Else {
# We're not, so likely in Azure Automation
Write-Output "Executing the runbook to send email about incomplete tasks..."
Connect-MgGraph -Identity -NoWelcome
# Email address to use when sending email from Azure Automation - update for your tenant
$MsgFrom = "no-reply@office365itpros.com"
}
# Add the recipient address to receive the report of deleted chat threads. Update this for your needs.
$EmailRecipient = "Customer.Services@Office365itpros.com"
# Check that we have the right permissions - in Azure Automation, we assume that the automation account has the right permissions
If ($Interactive) {
[int]$RequiredScopesCount = $RequiredScopes.Count
[string[]]$CurrentScopes = (Get-MgContext).Scopes
[string[]]$RequiredScopes = $RequiredScopes
$CheckScopes =[object[]][Linq.Enumerable]::Intersect($RequiredScopes,$CurrentScopes)
If ($CheckScopes.Count -ne $RequiredScopesCount ) {
Write-Host ("To run this script, you need to connect to Microsoft Graph with the following scopes: {0}" -f $RequiredScopes) -ForegroundColor Red
Break
}
}
# To sign in interactively with app-only access
# $AppId = '8af0f6ae-bb6c-416d-ab6b-45668a3c15ee'
# $TenantId = '72f988bf-86f1-4111-9a1a-0123456789ab'
# $CertificateThumbprint = '1234567890abcdef1234567890abcdef12345678
# Connect-MgGraph -ClientId "app-id" -TenantId "tenant-id" -CertificateThumbprint "thumbprint"
# Parameters for search
# Search only goes back a month - change this if you think it should go cover a different period.
# Define an array of chat thread topics that we want to find and remove
# Other filters are possible, but this is the easiest one to use to illustrate the principal
[array]$Topics = "Loopy Conversation", "Sensitive Stuff", "Project Aurora", "Supervision troubleshooting", "Inappropriate content"
# Compiled regex pattern to check if any of the topics are in the chat topic or message content. This is used to avoid having to loop through all the topics for each message. The regex will be used with the -match operator, which is case-insensitive by default.
$Pattern = ($Topics | ForEach-Object {[regex]::Escape($_)}) -join "|"
$Regex = [regex]::new($Pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
# Find users with Teams licenses
[guid]$TeamsServicePlanId = "57ff2da0-773e-42df-b2af-ffb7a2317929" #Teams
Try {
[array]$Users = Get-MgUser -filter "assignedPlans/any(s:s/serviceplanid eq $TeamsServicePlanId)" -Property Id, UserPrincipalName, DisplayName -ConsistencyLevel eventual -CountVariable Records -All -PageSize 500
Write-Output ("Found {0} user accounts to process..." -f $Users.Count)
$Users = $Users | Sort-Object -Property DisplayName
} Catch {
Write-Output "Error getting users: $_"
Break
}
$Report = [System.Collections.Generic.List[Object]]::new()
Write-Output "Processing users to find and remove chat threads with specific topics..."
ForEach ($User in $Users) {
# See if any matching chat threads exist for this user
Try {
# Find group or meeting topics created in the last month about the topics we're loooking for.
# Remove the filter for chat type if you also want to check one-on-one chats, but be aware that this will increase the number of chats that need to be checked and may cause the script to run for a long time if you have a lot of users and chat threads.
[array]$Chats = Get-MgUserChat -Userid $User.Id -All -Filter "(chatType eq 'Group' or chatType eq 'Meeting') and (createdDateTime ge $StartDate and createdDateTime le $EndDate) and (tenantId eq '$TenantId')"
} Catch {
Write-Output ("Error getting chats for user {0}: $_" -f $User.UserPrincipalName)
Continue
}
If ($Chats) {
Write-Host ("Checking {0} chats for user {1}" -f $Chats.Count, $User.displayName)
ForEach ($Chat in $Chats) {
$DeleteThread = $false
$MemberNamesOutput = $null
$TopicFound = $null
# Is the chat topic one of the ones we're looking for? If so, it should be deleted
If ($Chat.Topic -in $Topics) {
Write-Output ("Found chat with topic {0} for user {1}" -f $Chat.Topic, $User.displayName)
$DeleteThread = $true
$TopicFound = "Topic"
}
If ($DeleteThread -eq $false) {
# If not, we need to check the content of the messages in the thread to see if any of them contain the topic.
# The API doesn't allow us to filter messages based on content, so we have to get all messages and check them one by one.
# This is not ideal, but it's the only way to do it at the moment.
# Is the banned topic in the text of any of the messages in the thread?
[array]$Messages = Get-MgChatMessage -ChatId $Chat.Id -All
ForEach ($Message in $Messages) {
# Strip out HTML tags
$Content = ($Message.Body.Content -replace '<[^>]+>', '')
# Check messages to see if they contain any of the topics we're looking for. If so, the thread should be deleted. We only need one message to match to delete the thread, so we can stop checking after the first match.
If ($Regex.IsMatch($Content) -and $DeleteThread -eq $false) {
Write-Output ("Found chat message with matching content in chat {0} for user {1}" -f $Chat.Id, $User.displayName)
$DeleteThread = $true
$TopicFound = "Content"
}
}
}
If ($DeleteThread -eq $true) {
# Get members of the chat thread
[array]$Members = Get-MgUserChatMember -UserId $UserId -ChatId $Chat.Id
# Extract the member email addresses and remove any blanks (accounts that no longer exist)
[array]$MemberNames = $Members.additionalProperties.email | Sort-Object -Unique
$NumberOfParticipants = $MemberNames.Count
If ($MemberNames.Count -gt 0) {
$MemberNamesOutput = $MemberNames -Join ", "
}
# Soft-delete the chat thread
Write-Output ("Removing the chat thread {0}..." -f $Chat.Id)
Try {
Remove-MgChat -ChatId $Chat.Id
$ReportLine = [PSCustomObject][Ordered]@{
Timestamp = (Get-date)
Action = "Chat deleted"
User = $User.UserPrincipalName
UserId = $User.Id
ChatType = $Chat.ChatType
Topic = $Chat.Topic
"Problem found in" = $TopicFound
Participants = $MemberNamesOutput
NumberOfParticipants = $NumberOfParticipants
Created = $Chat.CreatedDateTime
LastUpdated = $Chat.LastUpdatedDateTime
TenantId = $Chat.TenantId
Id = $Chat.Id
}
# Update what we found
$Report.Add($ReportLine)
} Catch {
Write-Output ("Error removing chat {0}: $_" -f $Chat.Id)
}
# Wait a second to avoid throttling
Start-Sleep -Seconds 1
}
} # End Foreach Chats
} # End if Chats
} #End Foreach Users
$Report | Format-Table Timestamp, Action, User, Topic -AutoSize
# Set up to use the ImportExcel module to generate Excel worksheets if it is available. If not, the report will be generated in CSV format instead.
If (Get-Module ImportExcel -ListAvailable) {
$ExcelGenerated = $True
Import-Module ImportExcel -ErrorAction SilentlyContinue
}
# Generate the attachment in either Excel worksheet or CSV format, depending on if the ImportExcel module is available. If interactive, create
# the file in the user's Downloads folder, otherwise create it in the current directory.
If ($ExcelGenerated) {
If ($Interactive) {
$OutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\TeamsChatsRemoved.xlsx"
} Else {
$OutputFile = "TeamsChatsRemoved.xlsx"
}
$Report | Export-Excel -Path $OutputFile -WorksheetName "Teams Chats Removed" -Title ("Teams Chats Removed {0}" -f (Get-Date -format 'dd-MMM-yyyy')) -TitleBold -TableName "TeamsChatsRemoved"
} Else {
If ($Interactive) {
$OutputFile = ((New-Object -ComObject Shell.Application).Namespace('shell:Downloads').Self.Path) + "\TeamsChatsRemoved.csv"
} Else {
$OutputFile = "TeamsChatsRemoved.csv"
}
$Report | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding Utf8
}
Write-Host "Report generated in: $OutputFile" -ForegroundColor Green
$Attachment = (Get-Item -Path $OutputFile).Name
$EncodedAttachmentFile = [Convert]::ToBase64String([IO.File]::ReadAllBytes($OutputFile))
$MsgAttachments = @(
@{
"@odata.type" = "#microsoft.graph.fileAttachment"
Name = ($Attachment -split '\\')[-1]
ContentType = "application/vnd.ms-excel"
ContentBytes = $EncodedAttachmentFile
}
)
$ToRecipientList = @( $EmailRecipient )
[array]$MsgToRecipients = Add-MessageRecipients -ListOfAddresses $ToRecipientList
$MsgSubject = "Teams chat thread deletions"
$HtmlHead = "<h2>Teams chat thread deletions</h2><p>The following requests to remove chat threads have been processed.</p>"
$HtmlBody = $Report | ConvertTo-Html -Fragment
$HtmlMsg = "</body></html><p>" + $HtmlHead + $HtmlBody + "<p>"
# Construct the message body
$MsgBody = @{
Content = "$($HtmlMsg)"
ContentType = 'html'
}
$Message = @{}
$Message.Add("subject", $MsgSubject)
$Message.Add("toRecipients", $MsgToRecipients)
$Message.Add("body", $MsgBody)
$Message.Add("attachments", $MsgAttachments)
$EmailParameters = @{}
$EmailParameters.Add("message", $Message)
$EmailParameters.Add("saveToSentItems", $True)
$EmailParameters.Add("isDeliveryReceiptRequested", $True)
# And send the message using the parameters that we've filled in
Try {
Send-MgUserMail -UserId $MsgFrom -BodyParameter $EmailParameters
Write-Output ("Message containing deleted chat information sent to {0}!" -f $EmailRecipient)
} Catch {
Write-Output ("Error sending email: $_")
}

Parameters

ParameterDefaultNotes
-TenantId(Get-MgOrganization).IdMicrosoft Entra tenant ID for app-only Graph authentication.
-AppId""Application (client) ID for the app registration used to connect.
-StartDate(Get-Date).AddMonths(-1).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")Start of the reporting window.
-EndDate(Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")End of the reporting window.
Attribution