🔧 New: User Management Graph PowerShell Toolkit

Simplify user tasks like bulk creation, updates, password resets, deletions, license checks & more — all from one place.

🚀 Launch Toolkit

Email Private Teams List to Microsoft 365 Administrator Using Graph PowerShell

Private Teams are a powerful way to lock down collaboration, but without visibility, they can also become a governance blind spot. Over time, organizations may accumulate dozens or even hundreds of private Teams created for projects, sensitive discussions, or restricted departments. Without a clear overview of which private Teams exist and who owns them, admins can struggle to ensure proper oversight, security, and lifecycle management.

This script solves that by pulling a full list of all private Microsoft Teams, including owner details, saving it into a CSV, and emailing it directly to the administrator.


i) Script

# ===== Private 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   = "Private 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 PRIVATE Teams client-side ---
$privateTeams = $teams | Where-Object { $_.Visibility -eq "Private" }

# --- Build rows with owner details ---
$rows = foreach ($t in $privateTeams) {
$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 ("Private_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>Private Microsoft Teams Report</h3>
    <p>Total private 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
                            

ii) How the Script Works

  1. Connect to Microsoft Graph
  2. The script loads the Microsoft Graph module and connects using the required scopes:

    • Group.Read.All to list Teams.
    • User.Read.All to resolve owner details.
    • Mail.Send to email the report.
  3. Retrieve Teams-enabled groups
  4. It queries all Microsoft 365 groups where resourceProvisioningOptions contains "Team", ensuring only Teams-backed groups are returned.

  5. Filter to Private Teams
  6. Since the Graph API doesn’t support filtering on visibility directly, the script uses PowerShell’s Where-Object to pick Teams where Visibility -eq "Private".

  7. Gather Owners
  8. For each private Team, the script retrieves owner objects and resolves them into clean DisplayName and UserPrincipalName. Multiple owners are concatenated into semicolon-separated lists.

  9. Export to CSV and Email
  10. The details are exported to a timestamped CSV file, which is attached to an HTML summary email and sent to the administrator..


iii) Further Enhancements

  • Add Member Count: Include the number of members in each private Team.
  • Owner Count Column: Highlight Teams with zero owners for governance checks.
  • Include Activity Data: Enrich the report with usage statistics (active/inactive Teams).
  • Scheduled Automation: Run this script weekly/monthly using Task Scheduler or Azure Automation.
  • Archive Suggestions: Flag Teams older than a certain age with no recent activity.

iv) Use Cases

  • Security & Oversight: Ensure all private Teams are properly owned and monitored.
  • Governance Audits: Provide compliance officers with up-to-date reports.
  • Lifecycle Management: Identify stale or inactive private Teams for archiving.
  • Access Reviews: Verify ownership aligns with current organizational structure.

v) Possible Errors & Solutions

Error Cause Solution
Authorization_RequestDenied Missing Graph scopes or consent Reconnect with Group.Read.All, User.Read.All, Mail.Send and ensure consent is granted.
Get-MgGroup not recognized Microsoft Graph module not installed Install with Install-Module Microsoft.Graph -Scope CurrentUser.
Owners missing from CSV Owners are service principals / non-user objects Script falls back to AdditionalProperties; enrich only where possible.
Email not sent $FromUser not mailbox-enabled Use a licensed mailbox-enabled account for $FromUser.
Empty results No private Teams exist or visibility not populated Remove filter temporarily to confirm Teams are retrieved.

vi) Conclusion

Private Teams give organizations secure spaces for sensitive collaboration, but without oversight, they can easily become invisible to administrators. This script provides a complete inventory of all private Teams, along with their owners, packaged neatly into a CSV and emailed directly to IT. By scheduling and enhancing the script, you can strengthen governance, improve compliance, and maintain control of your Microsoft 365 environment.


Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                


                


                

© m365corner.com. All Rights Reserved. Design by HTML Codex