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 notIf ([Environment]::UserInteractive) {# We're running interactively...Clear-HostWrite-Host "Script running interactively... connecting to the Graph" -ForegroundColor YellowConnect-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 AutomationWrite-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 permissionsIf ($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 RedBreak}}# 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" #TeamsTry {[array]$Users = Get-MgUser -filter "assignedPlans/any(s:s/serviceplanid eq $TeamsServicePlanId)" -Property Id, UserPrincipalName, DisplayName -ConsistencyLevel eventual -CountVariable Records -All -PageSize 500Write-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 userTry {# 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 deletedIf ($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 -AllForEach ($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.CountIf ($MemberNames.Count -gt 0) {$MemberNamesOutput = $MemberNames -Join ", "}# Soft-delete the chat threadWrite-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.UserPrincipalNameUserId = $User.IdChatType = $Chat.ChatTypeTopic = $Chat.Topic"Problem found in" = $TopicFoundParticipants = $MemberNamesOutputNumberOfParticipants = $NumberOfParticipantsCreated = $Chat.CreatedDateTimeLastUpdated = $Chat.LastUpdatedDateTimeTenantId = $Chat.TenantIdId = $Chat.Id}# Update what we found$Report.Add($ReportLine)} Catch {Write-Output ("Error removing chat {0}: $_" -f $Chat.Id)}# Wait a second to avoid throttlingStart-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 = $TrueImport-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 inTry {Send-MgUserMail -UserId $MsgFrom -BodyParameter $EmailParametersWrite-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
Author
Office365itpros