Simplify user tasks like bulk creation, updates, password resets, deletions, license checks & more — all from one place.
🚀 Launch ToolkitWhen a tenant grows fast, public Teams can multiply without anyone noticing. They’re great for open collaboration—but if you don’t keep tabs on them, you risk oversharing, duplicated spaces, and governance blind spots.
The easiest fix is a reliable report you can scan (or hand to your security/governance folks) showing every public Team and who owns it. That’s exactly what the script below delivers: a clean CSV of all public Microsoft Teams with owner details, emailed straight to the administrator.
# ===== Public Teams -> CSV -> Email to Admin =====
# Requires: Microsoft.Graph module
# Scopes: Group.Read.All, User.Read.All, Mail.Send
# --- Email variables ---
$FromUser = "admin@contoso.com" # Sender (must have mailbox)
$To = "it-ops@contoso.com" # Recipient
$Subject = "Public Microsoft Teams report"
$CsvOutDir = "$env:TEMP"
# --- Connect to Microsoft Graph ---
Import-Module Microsoft.Graph -ErrorAction Stop
Connect-MgGraph -Scopes "Group.Read.All","User.Read.All","Mail.Send"
# --- Get Teams-enabled groups (can't filter by 'visibility' server-side) ---
$teams = Get-MgGroup -All -Filter "resourceProvisioningOptions/Any(x:x eq 'Team')" `
-Property "id,displayName,description,visibility,createdDateTime,mailNickname"
# --- Filter PUBLIC Teams client-side ---
$publicTeams = $teams | Where-Object { $_.Visibility -eq "Public" }
# --- Build rows with owner details ---
$rows = foreach ($t in $publicTeams) {
$ownerObjs = Get-MgGroupOwner -GroupId $t.Id -All -ErrorAction SilentlyContinue
$ownerNames = @()
$ownerUpns = @()
foreach ($o in $ownerObjs) {
try {
# Resolve to user to get clean DisplayName + UPN
$u = Get-MgUser -UserId $o.Id -Property DisplayName,UserPrincipalName -ErrorAction Stop
$ownerNames += $u.DisplayName
$ownerUpns += $u.UserPrincipalName
} catch {
# Fallback if non-user or missing fields
$dn = $o.AdditionalProperties['displayName']
$upn = $o.AdditionalProperties['userPrincipalName']
if ($dn) { $ownerNames += $dn }
if ($upn) { $ownerUpns += $upn }
}
}
[PSCustomObject]@{
TeamId = $t.Id
TeamName = $t.DisplayName
Description = $t.Description
Visibility = $t.Visibility
CreatedDate = $t.CreatedDateTime
OwnerNames = ($ownerNames -join "; ")
OwnerUPNs = ($ownerUpns -join "; ")
}
}
# --- Export to CSV ---
if (-not (Test-Path -Path $CsvOutDir)) { New-Item -ItemType Directory -Path $CsvOutDir | Out-Null }
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
$csvPath = Join-Path $CsvOutDir ("Public_Teams_{0}.csv" -f $ts)
$rows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
# --- Prepare HTML Body ---
$summaryHtml = @"
<html>
<body style='font-family:Segoe UI,Arial,sans-serif'>
<h3>Public Microsoft Teams Report</h3>
<p>Total public Teams: <b>$($rows.Count)</b></p>
<p>The full list (with owners) is attached as a CSV.</p>
</body>
</html>
"@
# --- Prepare Attachment ---
$fileBytes = [System.IO.File]::ReadAllBytes($csvPath)
$base64Content = [System.Convert]::ToBase64String($fileBytes)
$csvFileName = [System.IO.Path]::GetFileName($csvPath)
$attachment = @{
"@odata.type" = "#microsoft.graph.fileAttachment"
name = $csvFileName
contentBytes = $base64Content
contentType = "text/csv"
}
# --- Prepare and Send Email ---
$mail = @{
message = @{
subject = "${Subject}"
body = @{
contentType = "HTML"
content = $summaryHtml
}
toRecipients = @(@{ emailAddress = @{ address = $To } })
attachments = @($attachment)
}
saveToSentItems = $true
}
Send-MgUserMail -UserId $FromUser -BodyParameter $mail
Write-Host "Done. CSV saved at: $csvPath" -ForegroundColor Green
Uses Group.Read.All to list Teams, User.Read.All to resolve owners’ names/UPNs, and Mail.Send to email the CSV.
Since filtering by visibility isn’t supported server-side, the script first fetches Teams-enabled groups using the resourceProvisioningOptions filter.
Applies a local Where-Object to keep Teams where Visibility -eq "Public".
For each public Team, it fetches owners, resolves them to user objects for clean DisplayName and UPN, and aggregates multiple owners in semicolon-separated lists.
The CSV is timestamped for traceability and attached to an HTML email sent to the administrator.
Error | Cause | Solution |
---|---|---|
Request_UnsupportedQuery on visibility filter | Server doesn’t support filtering by visibility | Use the provided approach: fetch Teams first, then filter client-side. |
Authorization_RequestDenied | Missing/insufficient scopes or consent | Reconnect with Group.Read.All, User.Read.All, Mail.Send and ensure admin consent if required. |
Get-MgGroup not recognized | Microsoft Graph module not installed | Install-Module Microsoft.Graph -Scope CurrentUser then import. |
Owners missing in CSV | Some owners aren’t users (e.g., service principals) or properties unavailable | Script falls back to AdditionalProperties; enrich only where resolvable. |
Email not sent / sender issues | $FromUser has no mailbox or send rights | Use a mailbox-enabled account; verify it can send to $To. |
Empty results | No public Teams exist | Remove client-side filter temporarily to validate base Teams retrieval. |
Public Teams are fantastic for open collaboration—but they need oversight. This script gives you a dependable single-source report of every public Team plus its owners, delivered as a CSV to your inbox. Schedule it, share it with governance, and expand it over time (owner counts, settings, activity) to keep your Microsoft 365 environment tidy and safe.
© m365corner.com. All Rights Reserved. Design by HTML Codex