Public/NC.Groups.ps1
|
#Requires -Version 5.0 using namespace System.Management.Automation # Nebula.Core: Groups =============================================================================================================================== function Export-DistributionGroups { <# .SYNOPSIS Exports Exchange distribution group membership information. .DESCRIPTION Ensures an Exchange Online session, enumerates either all distribution groups or the provided identities, and gathers their members for CSV, GridView, or pipeline output. .PARAMETER DistributionGroup Distribution group identity (name, alias, or SMTP). Accepts pipeline input. .PARAMETER Csv Export the results to CSV (implied when -All or -CsvFolder is specified). .PARAMETER CsvFolder Destination folder for the CSV report. Defaults to the current directory. .PARAMETER All Export every distribution group in the tenant. .PARAMETER GridView Show the result in Out-GridView instead of returning objects. .EXAMPLE Export-DistributionGroups -DistributionGroup "Support Team" .EXAMPLE Export-DistributionGroups -All -CsvFolder C:\Temp #> [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('DG', 'Identity')] [string[]]$DistributionGroup, [switch]$Csv, [string]$CsvFolder, [switch]$All, [switch]$GridView ) begin { Set-ProgressAndInfoPreferences $requestedGroups = [System.Collections.Generic.List[string]]::new() } process { if ($DistributionGroup) { foreach ($entry in $DistributionGroup) { if (-not [string]::IsNullOrWhiteSpace($entry)) { [void]$requestedGroups.Add($entry.Trim()) } } } } end { try { if (-not (Test-EOLConnection)) { Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR return } $exportAll = $All.IsPresent -or $requestedGroups.Count -eq 0 $emitCsv = $Csv.IsPresent -or -not [string]::IsNullOrWhiteSpace($CsvFolder) -or $exportAll $folder = $null if ($emitCsv) { try { $folder = Test-Folder $CsvFolder } catch { Write-NCMessage "Invalid CSV folder. $($_.Exception.Message)" -Level ERROR return } } $groups = @() if ($exportAll) { try { $groups = Get-DistributionGroup -ResultSize Unlimited -WarningAction SilentlyContinue } catch { Write-NCMessage "Failed to retrieve distribution groups: $($_.Exception.Message)" -Level ERROR return } } else { foreach ($identity in ($requestedGroups.ToArray() | Select-Object -Unique)) { try { $groups += Get-DistributionGroup -Identity $identity -ErrorAction Stop } catch { Write-NCMessage "Distribution group '$identity' not found: $($_.Exception.Message)" -Level WARNING } } } if (-not $groups -or $groups.Count -eq 0) { Write-NCMessage "No distribution groups found matching the provided criteria." -Level WARNING return } $results = [System.Collections.Generic.List[object]]::new() $counter = 0 $total = $groups.Count foreach ($group in $groups) { $counter++ $percentComplete = (($counter / $total) * 100) Write-Progress -Activity "Processing $($group.DisplayName)" -Status "$counter of $total ($($percentComplete.ToString('0.00'))%)" -PercentComplete $percentComplete try { $members = @(Get-DistributionGroupMember -Identity $group.Identity -ResultSize Unlimited -ErrorAction Stop) } catch { Write-NCMessage "Failed to retrieve members for '$($group.DisplayName)': $($_.Exception.Message)" -Level WARNING continue } if (-not $members -or $members.Count -eq 0) { if ($exportAll) { $results.Add([pscustomobject][ordered]@{ 'Group Name' = $group.DisplayName 'Group Primary Smtp Address' = $group.PrimarySmtpAddress 'Member Display Name' = $null 'Member FirstName' = $null 'Member LastName' = $null 'Member Primary Smtp Address' = $null 'Member Company' = $null 'Member City' = $null }) | Out-Null } continue } foreach ($member in $members) { if ($exportAll) { $row = [ordered]@{ 'Group Name' = $group.DisplayName 'Group Primary Smtp Address' = $group.PrimarySmtpAddress 'Member Display Name' = $member.DisplayName 'Member FirstName' = $member.FirstName 'Member LastName' = $member.LastName 'Member Primary Smtp Address' = $member.PrimarySmtpAddress 'Member Company' = $member.Company 'Member City' = $member.City } } else { $row = [ordered]@{ 'Member Display Name' = $member.DisplayName 'Member FirstName' = $member.FirstName 'Member LastName' = $member.LastName 'Member Primary Smtp Address' = $member.PrimarySmtpAddress 'Member Company' = $member.Company 'Member City' = $member.City } } $results.Add([pscustomobject]$row) | Out-Null } } if (-not $results -or $results.Count -eq 0) { Write-NCMessage "No members found for the specified distribution groups." -Level WARNING return } if ($GridView.IsPresent) { $results | Out-GridView -Title "M365 Distribution Groups" } elseif ($emitCsv) { $csvPath = New-File("$($folder)\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-DistributionGroups-Report.csv") $results | Export-CSV -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $($NCVars.CSV_DefaultLimiter) Write-NCMessage "Distribution group membership exported to $csvPath." -Level SUCCESS } else { $results } } finally { Write-Progress -Activity "Processing distribution groups" -Completed Restore-ProgressAndInfoPreferences } } } function Export-DynamicDistributionGroups { <# .SYNOPSIS Exports dynamic distribution group membership information. .DESCRIPTION Ensures an Exchange Online session, enumerates either all dynamic distribution groups or the provided identities, and gathers their evaluated members for CSV, GridView, or output. .PARAMETER DynamicDistributionGroup Dynamic distribution group identity (name, alias, or SMTP). Accepts pipeline input. .PARAMETER Csv Export the results to CSV (implied when -All or -CsvFolder is specified). .PARAMETER CsvFolder Destination folder for the CSV report. Defaults to the current directory. .PARAMETER All Export every dynamic distribution group in the tenant. .PARAMETER GridView Show the result in Out-GridView instead of returning objects. .EXAMPLE Export-DynamicDistributionGroups -DynamicDistributionGroup "HR Auto" .EXAMPLE Export-DynamicDistributionGroups -All -CsvFolder C:\Temp #> [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('DDG', 'Identity')] [string[]]$DynamicDistributionGroup, [switch]$Csv, [string]$CsvFolder, [switch]$All, [switch]$GridView ) begin { Set-ProgressAndInfoPreferences $requestedGroups = [System.Collections.Generic.List[string]]::new() } process { if ($DynamicDistributionGroup) { foreach ($entry in $DynamicDistributionGroup) { if (-not [string]::IsNullOrWhiteSpace($entry)) { [void]$requestedGroups.Add($entry.Trim()) } } } } end { try { if (-not (Test-EOLConnection)) { Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR return } $exportAll = $All.IsPresent -or $requestedGroups.Count -eq 0 $emitCsv = $Csv.IsPresent -or -not [string]::IsNullOrWhiteSpace($CsvFolder) -or $exportAll $folder = $null if ($emitCsv) { try { $folder = Test-Folder $CsvFolder } catch { Write-NCMessage "Invalid CSV folder. $($_.Exception.Message)" -Level ERROR return } } $groups = @() if ($exportAll) { try { $groups = Get-DynamicDistributionGroup -ResultSize Unlimited -WarningAction SilentlyContinue } catch { Write-NCMessage "Failed to retrieve dynamic distribution groups: $($_.Exception.Message)" -Level ERROR return } } else { foreach ($identity in ($requestedGroups.ToArray() | Select-Object -Unique)) { try { $groups += Get-DynamicDistributionGroup -Identity $identity -ErrorAction Stop } catch { Write-NCMessage "Dynamic distribution group '$identity' not found: $($_.Exception.Message)" -Level WARNING } } } if (-not $groups -or $groups.Count -eq 0) { Write-NCMessage "No dynamic distribution groups found matching the provided criteria." -Level WARNING return } $results = [System.Collections.Generic.List[object]]::new() $counter = 0 $total = $groups.Count foreach ($group in $groups) { $counter++ $percentComplete = (($counter / $total) * 100) Write-Progress -Activity "Processing $($group.DisplayName)" -Status "$counter of $total ($($percentComplete.ToString('0.00'))%)" -PercentComplete $percentComplete try { $members = @(Get-DynamicDistributionGroupMember -Identity $group.Identity -ErrorAction Stop) } catch { Write-NCMessage "Failed to retrieve members for '$($group.DisplayName)': $($_.Exception.Message)" -Level WARNING continue } if (-not $members -or $members.Count -eq 0) { if ($exportAll) { $results.Add([pscustomobject][ordered]@{ 'Group Name' = $group.DisplayName 'Group Primary Smtp Address' = $group.PrimarySmtpAddress 'Member Display Name' = $null 'Member FirstName' = $null 'Member LastName' = $null 'Member Primary Smtp Address' = $null 'Member Company' = $null 'Member City' = $null }) | Out-Null } continue } foreach ($member in $members) { if ($exportAll) { $row = [ordered]@{ 'Group Name' = $group.DisplayName 'Group Primary Smtp Address' = $group.PrimarySmtpAddress 'Member Display Name' = $member.DisplayName 'Member FirstName' = $member.FirstName 'Member LastName' = $member.LastName 'Member Primary Smtp Address' = $member.PrimarySmtpAddress 'Member Company' = $member.Company 'Member City' = $member.City } } else { $row = [ordered]@{ 'Member Display Name' = $member.DisplayName 'Member FirstName' = $member.FirstName 'Member LastName' = $member.LastName 'Member Primary Smtp Address' = $member.PrimarySmtpAddress 'Member Company' = $member.Company 'Member City' = $member.City } } $results.Add([pscustomobject]$row) | Out-Null } } if (-not $results -or $results.Count -eq 0) { Write-NCMessage "No members found for the specified dynamic distribution groups." -Level WARNING return } if ($GridView.IsPresent) { $results | Out-GridView -Title "M365 Dynamic Distribution Groups" } elseif ($emitCsv) { $csvPath = New-File("$($folder)\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-DynamicDistributionGroups-Report.csv") $results | Export-CSV -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $($NCVars.CSV_DefaultLimiter) Write-NCMessage "Dynamic distribution group membership exported to $csvPath." -Level SUCCESS } else { $results } } finally { Write-Progress -Activity "Processing dynamic distribution groups" -Completed Restore-ProgressAndInfoPreferences } } } function Get-DynamicDistributionGroupFilter { <# .SYNOPSIS Returns a simplified filter definition for a Dynamic Distribution Group. .DESCRIPTION Fetches the raw RecipientFilter, strips the boilerplate clauses automatically appended by Exchange Online, normalizes whitespace/quoting, and outputs a copyable expression that can be reused when editing the group filter. .PARAMETER DynamicDistributionGroup Dynamic distribution group identity (name, alias, or SMTP). Accepts pipeline input. .PARAMETER IncludeDefaults Include the default Exchange filter clauses (SystemMailbox, CAS_, Audit mailboxes, etc.). .PARAMETER AsObject Return metadata (name, container, clause list) instead of just the normalized filter string. .EXAMPLE Get-DynamicDistributionGroupFilter -DynamicDistributionGroup "Campus Assago" .EXAMPLE "campusassago@tenant.onmicrosoft.com" | Get-DynamicDistributionGroupFilter -AsObject #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('DDG', 'Identity')] [string[]]$DynamicDistributionGroup, [switch]$IncludeDefaults, [switch]$AsObject ) begin { Set-ProgressAndInfoPreferences $requestedGroups = [System.Collections.Generic.List[string]]::new() } process { foreach ($entry in $DynamicDistributionGroup) { if (-not [string]::IsNullOrWhiteSpace($entry)) { [void]$requestedGroups.Add($entry.Trim()) } } } end { try { if (-not (Test-EOLConnection)) { Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR return } $targets = $requestedGroups | Select-Object -Unique if (-not $targets -or $targets.Count -eq 0) { Write-NCMessage "Specify at least one dynamic distribution group." -Level WARNING return } $defaultClauses = @( '-not Name -like "SystemMailbox{*"', '-not Name -like "CAS_{*"', '-not RecipientTypeDetailsValue -eq "MailboxPlan"', '-not RecipientTypeDetailsValue -eq "DiscoveryMailbox"', '-not RecipientTypeDetailsValue -eq "PublicFolderMailbox"', '-not RecipientTypeDetailsValue -eq "ArbitrationMailbox"', '-not RecipientTypeDetailsValue -eq "AuditLogMailbox"', '-not RecipientTypeDetailsValue -eq "AuxAuditLogMailbox"', '-not RecipientTypeDetailsValue -eq "SupervisoryReviewPolicyMailbox"' ) $normalizeFilter = { param($filter) if ([string]::IsNullOrWhiteSpace($filter)) { return $null } $value = $filter -replace "`r?`n", ' ' $value = $value -replace '[()]', ' ' $value = $value -replace '\s+-and\s+', ' -and ' $value = $value -replace '\s+-or\s+', ' -or ' $value = $value -replace '\s+-not\s+', ' -not ' $value = $value -replace '\s+', ' ' $value = $value.Replace("'", '"') return $value.Trim() } $splitClauses = { param($filter) if ([string]::IsNullOrWhiteSpace($filter)) { return @() } return ($filter -split ' -and ' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } foreach ($identity in $targets) { try { $group = Get-DynamicDistributionGroup -Identity $identity -ErrorAction Stop } catch { Write-NCMessage "Dynamic distribution group '$identity' not found: $($_.Exception.Message)" -Level WARNING continue } $rawFilter = $group.RecipientFilter if ([string]::IsNullOrWhiteSpace($rawFilter)) { Write-NCMessage "Recipient filter not available for '$($group.DisplayName)'." -Level WARNING continue } $normalizedFilter = & $normalizeFilter $rawFilter $clauses = & $splitClauses $normalizedFilter if (-not $IncludeDefaults.IsPresent -and $clauses.Count -gt 0) { $clauses = $clauses | Where-Object { $defaultClauses -notcontains $_ } } if (-not $clauses -or $clauses.Count -eq 0) { $clauses = & $splitClauses $normalizedFilter } $cleanedFilter = $clauses -join ' -and ' $result = [pscustomobject][ordered]@{ Name = $group.DisplayName Identity = $group.Identity RecipientContainer = $group.RecipientContainer Filter = $cleanedFilter RawFilter = $rawFilter FilterClauses = $clauses IncludeDefaults = $IncludeDefaults.IsPresent } if ($AsObject.IsPresent) { $result } else { $cleanedFilter } } } finally { Restore-ProgressAndInfoPreferences } } } function Export-M365Group { <# .SYNOPSIS Exports Microsoft 365 (Unified) group membership information. .DESCRIPTION Ensures an Exchange Online session, enumerates either all Microsoft 365 groups or the provided identities, and retrieves their members for CSV, GridView, or pipeline output. .PARAMETER M365Group Microsoft 365 group identity (name, alias, or SMTP). Accepts pipeline input. .PARAMETER Csv Export the results to CSV (implied when -All or -CsvFolder is specified). .PARAMETER CsvFolder Destination folder for the CSV report. Defaults to the current directory. .PARAMETER All Export every Microsoft 365 group in the tenant. .PARAMETER GridView Show the result in Out-GridView instead of returning objects. .EXAMPLE Export-M365Group -M365Group "Project Hub" .EXAMPLE Export-M365Group -All -CsvFolder C:\Temp #> [CmdletBinding()] param( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Group', 'Identity')] [string[]]$M365Group, [switch]$Csv, [string]$CsvFolder, [switch]$All, [switch]$GridView ) begin { Set-ProgressAndInfoPreferences $requestedGroups = [System.Collections.Generic.List[string]]::new() } process { if ($M365Group) { foreach ($entry in $M365Group) { if (-not [string]::IsNullOrWhiteSpace($entry)) { [void]$requestedGroups.Add($entry.Trim()) } } } } end { try { if (-not (Test-EOLConnection)) { Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR return } $exportAll = $All.IsPresent -or $requestedGroups.Count -eq 0 $emitCsv = $Csv.IsPresent -or -not [string]::IsNullOrWhiteSpace($CsvFolder) -or $exportAll $folder = $null if ($emitCsv) { try { $folder = Test-Folder $CsvFolder } catch { Write-NCMessage "Invalid CSV folder. $($_.Exception.Message)" -Level ERROR return } } $groups = @() if ($exportAll) { try { $groups = Get-UnifiedGroup -ResultSize Unlimited -WarningAction SilentlyContinue } catch { Write-NCMessage "Failed to retrieve Microsoft 365 groups: $($_.Exception.Message)" -Level ERROR return } } else { foreach ($identity in ($requestedGroups.ToArray() | Select-Object -Unique)) { try { $groups += Get-UnifiedGroup -Identity $identity -ErrorAction Stop } catch { Write-NCMessage "Microsoft 365 group '$identity' not found: $($_.Exception.Message)" -Level WARNING } } } if (-not $groups -or $groups.Count -eq 0) { Write-NCMessage "No Microsoft 365 groups found matching the provided criteria." -Level WARNING return } $results = [System.Collections.Generic.List[object]]::new() $counter = 0 $total = $groups.Count foreach ($group in $groups) { $counter++ $percentComplete = (($counter / $total) * 100) Write-Progress -Activity "Processing $($group.DisplayName)" -Status "$counter of $total ($($percentComplete.ToString('0.00'))%)" -PercentComplete $percentComplete try { $members = @(Get-UnifiedGroupLinks -Identity $group.Identity -LinkType Member -ErrorAction Stop) } catch { Write-NCMessage "Failed to retrieve members for '$($group.DisplayName)': $($_.Exception.Message)" -Level WARNING continue } if (-not $members -or $members.Count -eq 0) { if ($exportAll) { $results.Add([pscustomobject][ordered]@{ 'Group Name' = $group.DisplayName 'Group Primary Smtp Address' = $group.PrimarySmtpAddress 'Member Display Name' = $null 'Member FirstName' = $null 'Member LastName' = $null 'Member Primary Smtp Address' = $null 'Member Company' = $null 'Member City' = $null }) | Out-Null } continue } foreach ($member in $members) { if ($exportAll) { $row = [ordered]@{ 'Group Name' = $group.DisplayName 'Group Primary Smtp Address' = $group.PrimarySmtpAddress 'Member Display Name' = $member.DisplayName 'Member FirstName' = $member.FirstName 'Member LastName' = $member.LastName 'Member Primary Smtp Address' = $member.PrimarySmtpAddress 'Member Company' = $member.Company 'Member City' = $member.City } } else { $row = [ordered]@{ 'Member Display Name' = $member.DisplayName 'Member FirstName' = $member.FirstName 'Member LastName' = $member.LastName 'Member Primary Smtp Address' = $member.PrimarySmtpAddress 'Member Company' = $member.Company 'Member City' = $member.City } } $results.Add([pscustomobject]$row) | Out-Null } } if (-not $results -or $results.Count -eq 0) { Write-NCMessage "No members found for the specified Microsoft 365 groups." -Level WARNING return } if ($GridView.IsPresent) { $results | Out-GridView -Title "M365 Unified Groups" } elseif ($emitCsv) { $csvPath = New-File("$($folder)\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-UnifiedGroups-Report.csv") $results | Export-CSV -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $($NCVars.CSV_DefaultLimiter) Write-NCMessage "Microsoft 365 group membership exported to $csvPath." -Level SUCCESS } else { $results } } finally { Write-Progress -Activity "Processing Microsoft 365 groups" -Completed Restore-ProgressAndInfoPreferences } } } function Get-RoleGroupsMembers { <# .SYNOPSIS Lists Exchange Online role groups and their members. .DESCRIPTION Ensures an Exchange Online session, retrieves all role groups, collects members, and returns the data (optionally as a formatted table or GridView). .PARAMETER AsTable Display the output as a formatted table (default is to return objects). .PARAMETER GridView Show the result in Out-GridView instead of returning objects. .EXAMPLE Get-RoleGroupsMembers .EXAMPLE Get-RoleGroupsMembers -AsTable #> [CmdletBinding()] param( [switch]$AsTable, [switch]$GridView ) Set-ProgressAndInfoPreferences try { if (-not (Test-EOLConnection)) { Write-NCMessage "`nCan't connect or use Microsoft Exchange Online Management module. `nPlease check logs." -Level ERROR return } try { $roleGroups = Get-RoleGroup -ErrorAction Stop } catch { Write-NCMessage "Failed to retrieve role groups: $($_.Exception.Message)" -Level ERROR return } if (-not $roleGroups -or $roleGroups.Count -eq 0) { Write-NCMessage "No role groups found." -Level WARNING return } $results = [System.Collections.Generic.List[object]]::new() $counter = 0 $total = $roleGroups.Count foreach ($group in $roleGroups) { $counter++ $percentComplete = (($counter / $total) * 100) Write-Progress -Activity "Processing $($group.Name)" -Status "$counter of $total ($($percentComplete.ToString('0.00'))%)" -PercentComplete $percentComplete try { $members = @(Get-RoleGroupMember -Identity $group.Identity -ErrorAction Stop) } catch { Write-NCMessage "Failed to retrieve members for role group '$($group.Name)': $($_.Exception.Message)" -Level WARNING continue } $results.Add([pscustomobject][ordered]@{ 'Role Group' = $group.Name Count = if ($members) { $members.Count } else { 0 } Members = if ($members) { ($members.DisplayName -join "`n") } else { $null } }) | Out-Null } $sorted = $results | Sort-Object Count -Descending if ($GridView.IsPresent) { $sorted | Out-GridView -Title "Exchange Role Groups" } elseif ($AsTable.IsPresent) { Show-Table -Rows $sorted -AsTable } else { $sorted } } finally { Write-Progress -Activity "Processing role groups" -Completed Restore-ProgressAndInfoPreferences } } function Get-UserGroups { <# .SYNOPSIS Shows the Microsoft 365 groups that a user, contact, or distribution group belongs to. .DESCRIPTION Ensures Microsoft Graph (and Exchange Online) connectivity, resolves the provided identity via Get-Recipient, and uses Microsoft Graph to list every directory object membership. .PARAMETER UserPrincipalName User, contact, or group identity. Accepts display names, aliases, or e-mail addresses. .PARAMETER GridView Show additional details in Out-GridView instead of returning objects. .EXAMPLE Get-UserGroups -UserPrincipalName user@contoso.com .EXAMPLE 'user@contoso.com' | Get-UserGroups -GridView .NOTES Inspired by community samples originally published on infrasos.com. #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('User', 'Identity', 'UPN')] [string]$UserPrincipalName, [switch]$GridView ) begin { $graphConnected = $null } process { if ($null -eq $graphConnected) { $graphConnected = Test-MgGraphConnection if (-not $graphConnected) { Write-NCMessage "`nCan't connect or use Microsoft Graph modules. `nPlease check logs." -Level ERROR return } } $resolvedPrincipal = Find-UserRecipient -UserPrincipalName $UserPrincipalName if (-not $resolvedPrincipal) { Write-NCMessage "Unable to resolve user recipient for $UserPrincipalName" -Level ERROR return } $recipientType = (Get-Recipient -Identity $resolvedPrincipal).RecipientTypeDetails $memberships = @() switch ($recipientType) { 'MailContact' { try { $contact = Get-MgContact -Filter "Mail eq '$resolvedPrincipal'" -All -ErrorAction Stop | Select-Object -First 1 } catch { Write-NCMessage "Unable to resolve contact $resolvedPrincipal in Microsoft Graph: $($_.Exception.Message)" -Level ERROR return } if (-not $contact) { Write-NCMessage "Microsoft Graph contact not found for $resolvedPrincipal." -Level WARNING return } try { $memberships = @(Get-MgContactMemberOf -OrgContactId $contact.Id -All -ErrorAction Stop) } catch { Write-NCMessage "Unable to read group memberships for contact ${resolvedPrincipal}: $($_.Exception.Message)" -Level ERROR return } } 'MailUniversalDistributionGroup' { try { $group = Get-MgGroup -Filter "Mail eq '$resolvedPrincipal'" -All -ErrorAction Stop | Select-Object -First 1 } catch { Write-NCMessage "Unable to resolve group $resolvedPrincipal in Microsoft Graph: $($_.Exception.Message)" -Level ERROR return } if (-not $group) { Write-NCMessage "Microsoft Graph group not found for $resolvedPrincipal." -Level WARNING return } try { $memberships = @(Get-MgGroupMemberOf -GroupId $group.Id -All -ErrorAction Stop) } catch { Write-NCMessage "Unable to read memberships for group ${resolvedPrincipal}: $($_.Exception.Message)" -Level ERROR return } } default { $recipient = Get-Mailbox -Identity $resolvedPrincipal -ErrorAction Stop # To get WindowsLiveID when UPN differs / when Get-Recipient can't provide it $userId = if ($recipient.WindowsLiveID) { $recipient.WindowsLiveID } else { $resolvedPrincipal } try { $user = Get-MgUser -UserId $userId -ErrorAction Stop } catch { Write-NCMessage "Unable to resolve user $userId in Microsoft Graph: $($_.Exception.Message)" -Level ERROR return } try { $memberships = @(Get-MgUserMemberOf -UserId $user.Id -All -ErrorAction Stop) } catch { Write-NCMessage "Unable to read group memberships for ${resolvedPrincipal}: $($_.Exception.Message)" -Level ERROR return } } } Write-NCMessage "`n$recipientType ($resolvedPrincipal) - Groups found: $($memberships.Count)" -Level VERBOSE if (-not $memberships -or $memberships.Count -eq 0) { Write-NCMessage "No groups found for $resolvedPrincipal." -Level WARNING return } $results = [System.Collections.Generic.List[object]]::new() foreach ($membership in $memberships) { $props = if ($membership.AdditionalProperties) { $membership.AdditionalProperties } else { @{} } $row = [ordered]@{ 'Group Name' = if ($props.ContainsKey('displayName')) { $props.displayName } else { $null } 'Group Mail' = if ($props.ContainsKey('mail')) { $props.mail } else { $null } } if ($GridView.IsPresent) { $row['Group Description'] = if ($props.ContainsKey('description')) { $props.description } else { $null } $row['Group Mail Nickname'] = if ($props.ContainsKey('mailNickname')) { $props.mailNickname } else { $null } $row['Group Mail Enabled'] = if ($props.ContainsKey('mailEnabled')) { $props.mailEnabled } else { $null } $row['Group Type'] = if ($props.ContainsKey('groupTypes')) { ($props.groupTypes -join ', ') } else { $null } $row['Group ID'] = $membership.Id } $results.Add([pscustomobject]$row) | Out-Null } if ($GridView.IsPresent) { $results | Out-GridView -Title "M365 User Groups - $resolvedPrincipal" } else { $results | Sort-Object 'Group Name' } } } Set-Alias -Name Export-DG -Value Export-DistributionGroups Set-Alias -Name Export-DDG -Value Export-DynamicDistributionGroups Set-Alias -Name Get-DDGRecipientFilter -Value Get-DynamicDistributionGroupFilter |