Entra / Microsoft 365 ยท Teams
Teams groups activity report
A script to check the activity of Microsoft 365 Groups and Teams and report the groups and teams that might be deleted because they're not used.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
Connect-ExchangeOnlineConnect-SPOService -Url https://tenant-admin.sharepoint.com
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([int] $LookbackDays = 90)Clear-Host# Check that we are connected to Exchange Online, SharePoint Online, and TeamsWrite-Host "Checking that prerequisite PowerShell modules are loaded..."$ModulesLoaded = Get-Module | Select-Object NameIf (!($ModulesLoaded -match "ExchangeOnlineManagement")) {Write-Host "Please connect to the Exchange Online Management module and then restart the script"; break}If (!($ModulesLoaded -match "Microsoft.Online.SharePoint.PowerShell")) {Write-Host "Please connect to the SharePoint Online module and then restart the script"; break}# Comment these lines out if you don't want the script to create a temp directory to store its output files$path = "C:\Temp"If(!(test-path $path)) {New-Item -ItemType Directory -Force -Path $path | Out-Null }$OrgName = (Get-OrganizationConfig).Name# OK, we seem to be fully connected to both Exchange Online and SharePoint Online...Write-Host "Checking Microsoft 365 Groups and Teams in the tenant:" $OrgName# Setup some stuff we use$WarningDate = (Get-Date).AddDays(-$LookbackDays); $WarningEmailDate = (Get-Date).AddDays(-$LookbackDays); $Today = (Get-Date); $Date = $Today.ToShortDateString()$TeamsEnabled = $False; $ObsoleteSPOGroups = 0; $ObsoleteEmailGroups = 0$Report = [System.Collections.Generic.List[Object]]::new(); $ReportFile = "c:\temp\GroupsActivityReport.html"$CSVFile = "c:\temp\GroupsActivityReport.csv"$htmlhead="<html><style>BODY{font-family: Arial; font-size: 8pt;}H1{font-size: 22px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H2{font-size: 18px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}H3{font-size: 16px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}TABLE{border: 1px solid black; border-collapse: collapse; font-size: 8pt;}TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}TD{border: 1px solid #969595; padding: 5px; }td.pass{background: #B7EB83;}td.warn{background: #FFF275;}td.fail{background: #FF2626; color: #ffffff;}td.info{background: #85D4FF;}</style><body><div align=center><p><h1>Microsoft 365 Groups and Teams Activity Report</h1></p><p><h3>Generated: " + $date + "</h3></p></div>"# Get a list of Groups in the tenantWrite-Host "Extracting list of Microsoft 365 Groups for checking..."[Int]$GroupsCount = 0; [int]$TeamsCount = 0; $TeamsList = @{}; $UsedGroups = $False$Groups = Get-Recipient -RecipientTypeDetails GroupMailbox -ResultSize Unlimited | Sort-Object DisplayName$GroupsCount = $Groups.Count# If we don't find any groups (possible with Get-Recipient on a bad day), try to find them with Get-UnifiedGroup before giving up.If ($GroupsCount -eq 0) { #Write-Host "Fetching Groups using Get-UnifiedGroup"$Groups = Get-UnifiedGroup -ResultSize Unlimited | Sort-Object DisplayName$GroupsCount = $Groups.Count; $UsedGroups = $TrueIf ($GroupsCount -eq 0) {Write-Host "No Microsoft 365 Groups found; script exiting" ; break}} # End IfWrite-Host "Populating list of Teams..."If ($UsedGroups -eq $False) { # Populate the Teams hash table with a call to Get-UnifiedGroupGet-UnifiedGroup -Filter {ResourceProvisioningOptions -eq "Team"} -ResultSize Unlimited | `ForEach-Object { $TeamsList.Add($_.ExternalDirectoryObjectId, $_.DisplayName) }} Else { # We already have the $Groups variable populated with data, so extract the Teams from that data$Groups | Where-Object {$_.ResourceProvisioningOptions -eq "Team"} | `ForEach-Object { $TeamsList.Add($_.ExternalDirectoryObjectId, $_.DisplayName) }}$TeamsCount = $TeamsList.CountClear-Host# Set up progress bar$ProgDelta = 100/($GroupsCount); $CheckCount = 0; $GroupNumber = 0# Main loopForEach ($Group in $Groups) { #Because we fetched the list of groups with Get-Recipient, the first thing is to get the group properties$G = Get-UnifiedGroup -Identity $Group.DistinguishedName$GroupNumber++$GroupStatus = $G.DisplayName + " ["+ $GroupNumber +"/" + $GroupsCount + "]"Write-Progress -Activity "Checking group" -Status $GroupStatus -PercentComplete $CheckCount$CheckCount += $ProgDelta; $ObsoleteReportLine = $G.DisplayName; $SPOStatus = "Normal"$SPOActivity = "Document library in use"; $SPOStorage = 0$NumberWarnings = 0; $NumberofChats = 0; $TeamsEnabled = $False; $LastItemAddedtoTeams = "N/A"; $MailboxStatus = $Null; $ObsoleteReportLine = $Null# Check who manages the group$ManagedBy = $G.ManagedByIf ([string]::IsNullOrWhiteSpace($ManagedBy) -and [string]::IsNullOrEmpty($ManagedBy)) {$ManagedBy = "No owners"Write-Host $G.DisplayName "has no group owners!" -ForegroundColor Red}Else {$ManagedBy = (Get-ExoMailbox -Identity $G.ManagedBy[0]).DisplayName}# Group Age$GroupAge = (New-TimeSpan -Start $G.WhenCreated -End $Today).Days# Fetch information about activity in the Inbox folder of the group mailbox$Data = (Get-ExoMailboxFolderStatistics -Identity $G.ExternalDirectoryObjectId -IncludeOldestAndNewestITems -FolderScope Inbox)If ([string]::IsNullOrEmpty($Data.NewestItemReceivedDate)) {$LastConversation = "No items found"}Else {$LastConversation = Get-Date ($Data.NewestItemReceivedDate) -Format g }$NumberConversations = $Data.ItemsInFolder$MailboxStatus = "Normal"If ($Data.NewestItemReceivedDate -le $WarningEmailDate) {Write-Host "Last conversation item created in" $G.DisplayName "was" $Data.NewestItemReceivedDate "-> Obsolete?"$ObsoleteReportLine = $ObsoleteReportLine + " Last Outlook conversation dated: " + $LastConversation + "."$MailboxStatus = "Group Inbox Not Recently Used"$ObsoleteEmailGroups++$NumberWarnings++ }Else{# Some conversations exist - but if there are fewer than 20, we should flag this...If ($Data.ItemsInFolder -lt 20) {$ObsoleteReportLine = $ObsoleteReportLine + " Only " + $Data.ItemsInFolder + " Outlook conversation item(s) found."$MailboxStatus = "Low number of conversations"$NumberWarnings++}}# Loop to check audit records for activity in the group's SharePoint document libraryIf ($null -ne $G.SharePointSiteURL) {$SPOStorage = (Get-SPOSite -Identity $G.SharePointSiteUrl).StorageUsageCurrent$SPOStorage = [Math]::Round($SpoStorage/1024,2) # SharePoint site storage in GB$AuditCheck = $G.SharePointDocumentsUrl + "/*"$AuditRecs = $Null$AuditRecs = (Search-UnifiedAuditLog -RecordType SharePointFileOperation -StartDate $WarningDate -EndDate $Today -ObjectId $AuditCheck -ResultSize 1)If ($null -eq $AuditRecs) {#Write-Host "No audit records found for" $SPOSite.Title "-> Potentially obsolete!"$ObsoleteSPOGroups++$ObsoleteReportLine = $ObsoleteReportLine + " No SPO activity detected in the last 90 days." }}Else{# The SharePoint document library URL is blank, so the document library was never created for this group#Write-Host "SharePoint team site never created for the group" $G.DisplayName$ObsoleteSPOGroups++$AuditRecs = $Null$ObsoleteReportLine = $ObsoleteReportLine + " SPO document library never created."}# Report to the screen what we found - but only if something was found...If ($ObsoleteReportLine -ne $G.DisplayName){Write-Host $ObsoleteReportLine}# Generate the number of warnings to decide how obsolete the group might be...If ($null -eq $AuditRecs) {$SPOActivity = "No SPO activity detected in the last 90 days"$NumberWarnings++}If ($null -eq $G.SharePointDocumentsUrl) {$SPOStatus = "Document library never created"$NumberWarnings++}$Status = "Pass"If ($NumberWarnings -eq 1){$Status = "Warning"}If ($NumberWarnings -gt 1){$Status = "Fail"}# If the group is team-enabled, find the date of the last Teams conversation compliance recordIf ($TeamsList.ContainsKey($G.ExternalDirectoryObjectId) -eq $True) {$TeamsEnabled = $True[datetime]$DateOldTeams = "1-Jun-2021" # After this date, Microsoft should have moved the old Teams data to the new location$CountOldTeamsData = $False# Start by looking in the new location (TeamsMessagesData in Non-IPMRoot)$TeamsChatData = (Get-ExoMailboxFolderStatistics -Identity $G.ExternalDirectoryObjectId -IncludeOldestAndNewestItems -FolderScope NonIPMRoot | `Where-Object {$_.FolderType -eq "TeamsMessagesData" })If ($TeamsChatData.ItemsInFolder -gt 0) {$LastItemAddedtoTeams = Get-Date ($TeamsChatData.NewestItemReceivedDate) -Format g}$NumberOfChats = $TeamsChatData.ItemsInFolder# If the script is running before 1-Jun-2021, we need to check the old location of the Teams compliance recordsIf ($Today -lt $DateOldTeams) {$CountOldTeamsData = $True$OldTeamsChatData = (Get-ExoMailboxFolderStatistics -Identity $G.ExternalDirectoryObjectId -IncludeOldestAndNewestItems -FolderScope ConversationHistory)ForEach ($T in $OldTeamsChatData) { # We might have one or two subfolders in Conversation History; find the one for TeamsIf ($T.FolderType -eq "TeamChat") {If ($T.ItemsInFolder -gt 0) {$OldLastItemAddedtoTeams = Get-Date ($T.NewestItemReceivedDate) -Format g}$OldNumberofChats = $T.ItemsInFolder}}}If ($CountOldTeamsData -eq $True) { # We have counted the old date, so let's put the two sets together$NumberOfChats = $NumberOfChats + $OldNumberOfChatsIf (!$LastItemAddedToTeams) { $LastItemAddedToTeams = $OldLastItemAddedToTeams }} # End ifIf (($TeamsEnabled -eq $True) -and ($NumberOfChats -le 100)) { Write-Host "Team-enabled group" $G.DisplayName "has only" $NumberOfChats "compliance record(s)" }} # End if Processing Teams data# Generate a line for this group and store it in the report$ReportLine = [PSCustomObject][Ordered]@{GroupName = $G.DisplayNameManagedBy = $ManagedByMembers = $G.GroupMemberCountExternalGuests = $G.GroupExternalMemberCountDescription = $G.NotesMailboxStatus = $MailboxStatusTeamEnabled = $TeamsEnabledLastChat = $LastItemAddedtoTeamsNumberChats = $NumberofChatsLastConversation = $LastConversationNumberConversations = $NumberConversationsSPOActivity = $SPOActivitySPOStorageGB = $SPOStorageSPOStatus = $SPOStatusWhenCreated = Get-Date ($G.WhenCreated) -Format gDaysOld = $GroupAgeNumberWarnings = $NumberWarningsStatus = $Status}$Report.Add($ReportLine)#End of main loop}If ($TeamsCount -gt 0) { # We have some teams, so we can calculate a percentage of Team-enabled groups$PercentTeams = ($TeamsCount/$GroupsCount)$PercentTeams = ($PercentTeams).tostring("P") }Else {$PercentTeams = "No teams found" }# Create the HTML report$htmlbody = $Report | ConvertTo-Html -Fragment$htmltail = "<p>Report created for: " + $OrgName + "</p><p>Number of groups scanned: " + $GroupsCount + "</p>" +"<p>Number of potentially obsolete groups (based on document library activity): " + $ObsoleteSPOGroups + "</p>" +"<p>Number of potentially obsolete groups (based on conversation activity): " + $ObsoleteEmailGroups + "<p>"+"<p>Number of Teams-enabled groups : " + $TeamsCount + "</p>" +"<p>Percentage of Teams-enabled groups: " + $PercentTeams + "</body></html>" +"<p>-----------------------------------------------------------------------------------------------------------------------------"+"<p>Microsoft 365 Groups and Teams Activity Report <b>V4.8</b>"$htmlreport = $htmlhead + $htmlbody + $htmltail$htmlreport | Out-File $ReportFile -Encoding UTF8$Report | Export-CSV -NoTypeInformation $CSVFile$Report | Out-GridView# Summary noteClear-HostWrite-Host " "Write-Host "Results"Write-Host "-------"Write-Host "Number of Microsoft 365 Groups scanned :" $GroupsCountWrite-Host "Potentially obsolete groups (based on document library activity):" $ObsoleteSPOGroupsWrite-Host "Potentially obsolete groups (based on conversation activity) :" $ObsoleteEmailGroupsWrite-Host "Number of Teams-enabled groups :" $TeamsList.CountWrite-Host "Percentage of Teams-enabled groups :" $PercentTeamsWrite-Host " "Write-Host "Summary report in" $ReportFile "and CSV in" $CSVFile
Parameters
ParameterDefaultNotes
-LookbackDays90How many days back to search for newly created mailboxes or recent activity.Attribution
Author
Office365itpros