I Audited 3,800 M365 Groups for Conversations. My Scripts Had a Blind Spot.

Microsoft 365 Groups can host email conversations through a distinct Graph API that standard group queries never touch. If your lifecycle policies, audit scripts, or eDiscovery workflows don't query the conversations endpoint, you have…

Microsoft 365 Groups were sold to us as Teams channels and SharePoint sites. That’s the mental model every estate audit runs on. It’s also wrong often enough to matter, and I found out the boring way: a legal hold that came back incomplete.

0

That’s how many email threads Get-MgGroup and its usual companions will ever show you. Not a few. Zero. The conversations an Outlook group hosts — the threaded email discussions that predate Teams and still run quietly in plenty of departments — live on a completely different resource path: /groups/{id}/threads and /groups/{id}/conversations, with individual messages hanging off those as posts. The standard groups API doesn’t surface them because they aren’t group properties. They’re group contents, and nothing in the membership-and-settings view you normally pull touches them.

So every “complete” group inventory I’d shipped was complete only in the sense that it covered what I’d asked for.

3,800

The number of Groups in the tenant I was auditing. The standard reports — members, owners, SharePoint quota, Teams-enabled flag — all ran clean. Then I enumerated threads per group and the picture changed. A tenant that thinks of itself as “all-in on Teams” was carrying hundreds of groups with live email conversation history, most created in the Outlook era and never migrated. Finance had one. A regional sales team had one with years of threads. Nobody had decided to keep these alive; nobody had decided to kill them either. They just kept receiving mail.

2 words that should scare you: legal hold

Here’s where the blind spot stops being academic. Your audit tooling, your offboarding scripts, your “what collaboration artifacts does this person touch” reports—if they’re built on the groups API, they report these conversations as empty. When an eDiscovery request lands and you attest that you’ve identified all relevant collaboration surfaces, “I queried the groups endpoint” is not the same statement as “I queried the conversations endpoint.” One of those gets you burned in six months.

The same gap bites lifecycle policy. An inactivity-based expiration policy might flag a group as dormant because nobody’s touched the SharePoint site or Teams — while email threads are still arriving in the group mailbox. Expire it, and you’ve just deleted active correspondence that someone, somewhere, considered their primary channel.

3 endpoints, read-only, paginated

So inventory them before you make any decision. This walks every group, counts threads, and surfaces the ones that actually have email activity. It changes nothing — no -WhatIf needed because it never writes. Export the result, review it, and then decide what feeds a lifecycle or retention action.

audit.ps1PowerShell
Connect-MgGraph -Scopes 'Group.Read.All' -NoWelcome
​
$results = [System.Collections.Generic.List[object]]::new()
​
# Pull every unified (M365) group with pagination handled by -All
$groups = Get-MgGroup -All -Property Id,DisplayName,GroupTypes,Mail `
    -Filter "groupTypes/any(g:g eq 'Unified')"
​
foreach ($group in $groups) {
    try {
        # Threads live on a distinct resource path, not on the group object
        $threads = Get-MgGroupThread -GroupId $group.Id -All -ErrorAction Stop
​
        if ($threads.Count -gt 0) {
            $results.Add([pscustomobject]@{
                DisplayName = $group.DisplayName
                Mail        = $group.Mail
                GroupId     = $group.Id
                ThreadCount = $threads.Count
                LastPost    = ($threads |
                    Sort-Object LastDeliveredDateTime -Descending |
                    Select-Object -First 1).LastDeliveredDateTime
            })
        }
    }
    catch {
        Write-Warning "Failed to read threads for $($group.DisplayName) [$($group.Id)]: $($_.Exception.Message)"
        continue
    }
}
​
$results | Sort-Object ThreadCount -Descending |
    Export-Csv -Path '.\GroupsWithConversations.csv' -NoTypeInformation
Write-Host "Found $($results.Count) group(s) hosting email conversations."

-All handles pagination on both the group list and the thread list, so you won’t silently truncate at the default page size — a classic way to under-report and call it a clean audit. The try/catch matters because some groups will throw on the threads call for permission or provisioning reasons, and you want that logged, not swallowed.

Where it lives

There’s no “Conversations” blade in the admin center, which is half the reason this stays invisible. The groups themselves sit under Entra admin center → Groups; the mailbox and retention behaviour is governed in Exchange Online and Purview. The conversation data itself is Graph-only. That’s not a gap Microsoft is in a hurry to close, so the burden’s on you.

Email-based Groups aren’t dead and they aren’t deprecated. They’re just quiet — and quiet is exactly what an auditor should distrust. Run the query. Keep the CSV. The next hold request will ask for it.