Public/NC.Mailboxes.ps1
|
#Requires -Version 5.0 using namespace System.Management.Automation # Nebula.Core: Mailboxes helpers ==================================================================================================================== function Add-MboxAlias { <# .SYNOPSIS Adds a new alias to a mailbox or mail-enabled recipient. .DESCRIPTION Validates Exchange Online connectivity, checks the recipient, and adds the alias when it does not already exist. .PARAMETER SourceMailbox Mailbox or recipient identity. Accepts pipeline input. .PARAMETER MailboxAlias Alias to add (SMTP address). .EXAMPLE Add-MboxAlias -SourceMailbox user@contoso.com -MailboxAlias alias@contoso.com #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string]$SourceMailbox, [Parameter(Mandatory)] [string]$MailboxAlias ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } try { $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop } catch { Write-NCMessage "Mailbox or recipient '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR return } $normalizedAlias = $MailboxAlias.ToLowerInvariant() $existingAliases = $recipient.EmailAddresses | ForEach-Object { ($_ -replace '^smtp:', '').ToLowerInvariant() } if ($existingAliases -contains $normalizedAlias) { Write-NCMessage ("Alias '{0}' already exists for '{1}'. No action taken." -f $MailboxAlias, $recipient.PrimarySmtpAddress) -Level WARNING return } try { switch ($recipient.RecipientTypeDetails) { 'MailContact' { Set-MailContact -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop } 'MailUser' { Set-MailUser -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop } {($_ -eq 'MailUniversalDistributionGroup') -or ($_ -eq 'DynamicDistributionGroup') -or ($_ -eq 'MailUniversalSecurityGroup')} { Set-DistributionGroup -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop } default { Set-Mailbox -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop } } Write-NCMessage ("Alias '{0}' added to {1}." -f $MailboxAlias, $recipient.PrimarySmtpAddress) -Level SUCCESS } catch { Write-NCMessage "Unable to add alias '$MailboxAlias' to '$($recipient.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR return } Get-MboxAlias -SourceMailbox $recipient.PrimarySmtpAddress } end { Restore-ProgressAndInfoPreferences } } function Add-MboxPermission { <# .SYNOPSIS Grants mailbox permissions to users. .DESCRIPTION Ensures Exchange Online connectivity, validates target and user mailboxes, and assigns FullAccess, SendAs, SendOnBehalfTo, or both (All). Returns the applied permissions. .PARAMETER SourceMailbox Mailbox identity to update. .PARAMETER UserMailbox One or more users to grant permissions to. Accepts pipeline input. .PARAMETER AccessRights Permission type: All, FullAccess, SendAs, SendOnBehalfTo. Defaults to All (FullAccess + SendAs). .PARAMETER AutoMapping Enable Outlook automapping when granting FullAccess. .PARAMETER PassThru When specified, emits detailed permission objects to the pipeline. .EXAMPLE Add-MboxPermission -SourceMailbox user@contoso.com -UserMailbox user@contoso.com -AccessRights FullAccess -AutoMapping .EXAMPLE Add-MboxPermission -SourceMailbox user@contoso.com -UserMailbox user@contoso.com -PassThru #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string]$SourceMailbox, [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]]$UserMailbox, [ValidateSet('All', 'FullAccess', 'SendAs', 'SendOnBehalfTo')] [string]$AccessRights = 'All', [switch]$AutoMapping, [switch]$PassThru ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } try { $targetMailbox = Get-Mailbox -Identity $SourceMailbox -ErrorAction Stop } catch { Write-NCMessage "Mailbox '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR return } foreach ($user in $UserMailbox) { try { $userObject = Get-User -Identity $user -ErrorAction Stop } catch { Add-EmptyLine Write-NCMessage "The mailbox '$user' does not exist. Please check the provided e-mail address." -Level ERROR continue } $userIdentity = $userObject.UserPrincipalName switch ($AccessRights) { 'FullAccess' { $existingPermission = Get-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'FullAccess' -and -not $_.IsInherited } if ($existingPermission) { Write-NCMessage ("{0} already has FullAccess permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING continue } $added = Add-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -AutoMapping:$AutoMapping.IsPresent -Confirm:$false Write-NCMessage ("Added FullAccess for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS if ($PassThru) { [pscustomobject]@{ Identity = $added.Identity User = $added.User DisplayName = $userObject.DisplayName AccessRights = $added.AccessRights IsInherited = $added.IsInherited Deny = $added.Deny } } } 'SendAs' { $existingPermission = Get-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'SendAs' } if ($existingPermission) { Write-NCMessage ("{0} already has SendAs permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING continue } $added = Add-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false Write-NCMessage ("Added SendAs for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS if ($PassThru) { [pscustomobject]@{ Identity = $added.Identity Trustee = $added.Trustee DisplayName = $userObject.DisplayName AccessControlType = $added.AccessControlType AccessRights = $added.AccessRights } } } 'SendOnBehalfTo' { $existingPermission = $targetMailbox.GrantSendOnBehalfTo | Where-Object { $_ -eq $userIdentity } if ($existingPermission) { Write-NCMessage ("{0} already has SendOnBehalfTo permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING continue } Set-Mailbox -Identity $targetMailbox.PrimarySmtpAddress -GrantSendOnBehalfTo @{ add = $userIdentity } -Confirm:$false | Out-Null Write-NCMessage ("Added SendOnBehalfTo for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS if ($PassThru) { [pscustomobject]@{ Identity = $targetMailbox.PrimarySmtpAddress Trustee = $userIdentity DisplayName = $userObject.DisplayName AccessRights = 'SendOnBehalfTo' } } } 'All' { $created = @() $existingFullAccess = Get-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'FullAccess' -and -not $_.IsInherited } if (-not $existingFullAccess) { $added = Add-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -AutoMapping:$AutoMapping.IsPresent -Confirm:$false Write-NCMessage ("Added FullAccess for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS $created += [pscustomobject]@{ Identity = $added.Identity User = $added.User DisplayName = $userObject.DisplayName AccessRights = $added.AccessRights IsInherited = $added.IsInherited Deny = $added.Deny } } else { Write-NCMessage ("{0} already has FullAccess permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING } $existingSendAs = Get-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'SendAs' } if (-not $existingSendAs) { $addedSendAs = Add-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false Write-NCMessage ("Added SendAs for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS $created += [pscustomobject]@{ Identity = $addedSendAs.Identity Trustee = $addedSendAs.Trustee DisplayName = $userObject.DisplayName AccessControlType = $addedSendAs.AccessControlType AccessRights = $addedSendAs.AccessRights } } else { Write-NCMessage ("{0} already has SendAs permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING } if ($PassThru) { $created } } } } } end { Restore-ProgressAndInfoPreferences } } function Export-MboxPermission { <# .SYNOPSIS Exports mailbox permissions for selected recipient types. .DESCRIPTION Gathers FullAccess, SendAs, and SendOnBehalfTo permissions for user, shared, room, or all mailboxes and writes them to a CSV report. .PARAMETER RecipientType Recipient type to analyze: User, Shared, Room, All. .PARAMETER CsvFolder Destination folder for the CSV file. Defaults to current directory. .PARAMETER BatchSize Number of processed mailboxes before flushing partial CSV output. .PARAMETER Resume Resume from the latest matching CSV in the target folder or from -CsvPath. .PARAMETER CsvPath Explicit CSV file to resume. When omitted, the most recent matching CSV in the target folder is used. .PARAMETER MaxConsecutiveErrors Stop after this many consecutive mailbox-level failures. .EXAMPLE Export-MboxPermission -RecipientType All -CsvFolder C:\Temp #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('User', 'Shared', 'Room', 'All')] [string]$RecipientType, [string]$CsvFolder, [ValidateRange(1, 500)] [int]$BatchSize = 25, [switch]$Resume, [string]$CsvPath, [ValidateRange(1, 100)] [int]$MaxConsecutiveErrors = 5 ) begin { Set-ProgressAndInfoPreferences $permissions = [System.Collections.Generic.List[object]]::new() $processedSinceFlush = 0 $consecutiveErrors = 0 $aborted = $false } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } switch ($RecipientType) { 'User' { $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -eq 'UserMailbox' } } 'Shared' { $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -eq 'SharedMailbox' } } 'Room' { $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -eq 'RoomMailbox' } } 'All' { Write-NCMessage "No recipient type specified, scanning User, Shared, and Room mailboxes." -Level WARNING $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -in @('UserMailbox', 'SharedMailbox', 'RoomMailbox') } } } $normalizeText = { param($value) return Get-NormalizedText -Value $value } $buildPermissionKey = { param($row) $mailbox = & $normalizeText $row.'Mailbox Address' if (-not $mailbox) { $mailbox = & $normalizeText $row.Mailbox } $recipientType = & $normalizeText $row.'Recipient Type' return "{0}|{1}" -f $mailbox, $recipientType } $existingPermissionKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $csvPathResolved = $null $folderForExport = Test-Folder $CsvFolder $defaultCsvPath = New-File "$folderForExport\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-MboxPermissions-Report.csv" $csvPathResolved = $defaultCsvPath if ($Resume) { $resumePath = $null if (-not [string]::IsNullOrWhiteSpace($CsvPath)) { $resumePath = $CsvPath } else { $existingCsv = Get-ChildItem -LiteralPath $folderForExport -File -Filter "*_M365-MboxPermissions-Report.csv" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($existingCsv) { $resumePath = $existingCsv.FullName } } if ($resumePath) { $csvPathResolved = $resumePath if (Test-Path -LiteralPath $csvPathResolved) { try { foreach ($row in (Import-CSV -LiteralPath $csvPathResolved -Delimiter $NCVars.CSV_DefaultLimiter -ErrorAction Stop)) { $null = $existingPermissionKeys.Add((& $buildPermissionKey $row)) } Write-NCMessage ("Resuming mailbox permissions export from {0}; {1} row(s) already recorded." -f $csvPathResolved, $existingPermissionKeys.Count) -Level INFO } catch { Write-NCMessage ("Unable to read existing CSV '{0}' for resume. {1}" -f $csvPathResolved, $_.Exception.Message) -Level WARNING $existingPermissionKeys.Clear() $csvPathResolved = $defaultCsvPath } } else { Write-NCMessage ("Resume requested for '{0}', but the file does not exist. Starting a new report at that path." -f $csvPathResolved) -Level INFO } } else { Write-NCMessage ("Resume requested, but no existing CSV was found. Starting a new report at {0}." -f $csvPathResolved) -Level INFO } } Write-NCMessage ("Mailbox permission export will flush every {0} mailbox(es). Resume: {1}. Stop after {2} consecutive error(s)." -f $BatchSize, $Resume.IsPresent, $MaxConsecutiveErrors) -Level INFO Write-NCMessage "Saving report to $csvPathResolved" -Level DEBUG $writeBuffer = { param([System.Collections.Generic.List[object]]$buffer) if ($buffer.Count -eq 0) { return } $exportRows = $buffer if ((Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) { $exportRows | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter -Append } else { $exportRows | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter } $buffer.Clear() } $counter = 0 foreach ($mailbox in $mailboxes) { $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $mailboxes.Count Write-Progress -Activity "Processing $($mailbox.PrimarySmtpAddress)" -Status "$counter of $($mailboxes.Count) - $Percentage%" -PercentComplete $Percentage try { $exoMailbox = Get-EXOMailbox -Identity $mailbox.Identity -ErrorAction Stop $sendAs = Get-RecipientPermission -Identity $exoMailbox.PrimarySmtpAddress -AccessRights SendAs -ErrorAction Stop | Where-Object { $_.Trustee.ToString() -ne 'NT AUTHORITY\SELF' -and $_.Trustee.ToString() -notlike 'S-1-5*' } | ForEach-Object { $_.Trustee.ToString() } $fullAccess = Get-MailboxPermission -Identity $exoMailbox.PrimarySmtpAddress -ErrorAction Stop | Where-Object { $_.AccessRights -eq 'FullAccess' -and -not $_.IsInherited } | ForEach-Object { $_.User.ToString() } $row = [pscustomobject]@{ Mailbox = $exoMailbox.DisplayName 'Mailbox Address' = $exoMailbox.PrimarySmtpAddress 'Recipient Type' = $exoMailbox.RecipientTypeDetails FullAccess = ($fullAccess -join ', ') SendAs = ($sendAs -join ', ') SendOnBehalfTo = $exoMailbox.GrantSendOnBehalfTo -join ', ' } } catch { Write-NCMessage "Failed to process mailbox '$($mailbox.PrimarySmtpAddress)': $($_.Exception.Message)" -Level ERROR $consecutiveErrors++ if ($MaxConsecutiveErrors -gt 0 -and $consecutiveErrors -ge $MaxConsecutiveErrors) { $aborted = $true break } continue } if ($Resume -and $existingPermissionKeys.Contains((& $buildPermissionKey $row))) { continue } $null = $existingPermissionKeys.Add((& $buildPermissionKey $row)) $permissions.Add($row) | Out-Null $consecutiveErrors = 0 $processedSinceFlush++ if ($BatchSize -gt 0 -and $permissions.Count -gt 0 -and $processedSinceFlush -ge $BatchSize) { & $writeBuffer $permissions $processedSinceFlush = 0 } } } end { try { if ($permissions.Count -eq 0) { if ($Resume -and (Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) { Write-NCMessage "No new mailbox permissions found. Existing CSV at $csvPathResolved already contains the requested rows." -Level INFO } else { Write-NCMessage "No mailbox permissions were collected." -Level WARNING } return } & $writeBuffer $permissions if ($aborted) { Write-NCMessage "Mailbox permissions export stopped early. Partial data kept at $csvPathResolved." -Level ERROR } else { Write-NCMessage "Mailbox permissions exported to $csvPathResolved" -Level SUCCESS } } finally { Write-Progress -Activity "Processing mailbox permissions" -Completed Restore-ProgressAndInfoPreferences } } } function Get-MboxAlias { <# .SYNOPSIS Lists primary and secondary SMTP addresses for a recipient. .DESCRIPTION Ensures Exchange Online connectivity and returns aliases with a flag for the primary address. Use -Csv for single mailbox exports. Use -All or -Domain to export a tenant-wide or domain-scoped CSV report with one row per alias. CSV exports include DisplayName and Name, omit recipients that have only the primary address unless -IncludePrimaryOnly is used, and hide MOERA addresses unless -IncludeMoera is used. .PARAMETER SourceMailbox Mailbox or recipient identity. Accepts pipeline input. .PARAMETER Csv Export a single mailbox result set to CSV. .PARAMETER CsvFolder Destination folder for the CSV file. Defaults to current directory. .PARAMETER All Enumerate every non-guest recipient and export the aliases to CSV. .PARAMETER Domain Enumerate recipients whose addresses match the provided domain and export the aliases to CSV. .PARAMETER IncludePrimaryOnly Include recipients that have no secondary aliases in CSV exports. .PARAMETER IncludeMoera Include MOERA addresses in CSV exports. .PARAMETER BatchSize Number of processed recipients before flushing partial CSV output. .PARAMETER Resume Resume from the latest matching CSV in the target folder or from -CsvPath. .PARAMETER CsvPath Explicit CSV file to resume. When omitted, the most recent matching CSV in the target folder is used. .PARAMETER MaxConsecutiveErrors Stop after this many consecutive recipient-level failures. .EXAMPLE Get-MboxAlias -SourceMailbox user@contoso.com .EXAMPLE Get-MboxAlias user@contoso.com .EXAMPLE Get-MboxAlias -All -CsvFolder C:\Temp .EXAMPLE Get-MboxAlias -SourceMailbox user@contoso.com -Csv -IncludeMoera -CsvFolder C:\Temp #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Single')] [Alias('Identity')] [string]$SourceMailbox, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [switch]$Csv, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [string]$CsvFolder, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [switch]$IncludePrimaryOnly, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [switch]$IncludeMoera, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [ValidateRange(1, 500)] [int]$BatchSize = 25, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [switch]$Resume, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [string]$CsvPath, [Parameter(ParameterSetName = 'Single')] [Parameter(ParameterSetName = 'All')] [Parameter(ParameterSetName = 'Domain')] [ValidateRange(1, 100)] [int]$MaxConsecutiveErrors = 5, [Parameter(Mandatory, ParameterSetName = 'All')] [switch]$All, [Parameter(Mandatory, ParameterSetName = 'Domain')] [string]$Domain ) begin { Set-ProgressAndInfoPreferences $aliasRows = [System.Collections.Generic.List[object]]::new() $resolvedCsvFolder = $null $processedSinceFlush = 0 $consecutiveErrors = 0 $aborted = $false } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } if ($PSCmdlet.ParameterSetName -eq 'Single') { try { $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop } catch { Write-NCMessage "Recipient '$SourceMailbox' not available or not found." -Level ERROR $consecutiveErrors++ return } $recipients = @($recipient) } else { if ($All) { Write-NCMessage "No mailbox specified; scanning all recipients. This may take a while." -Level WARNING Write-Progress -Activity "Processing aliases" -Status "Retrieving recipient list ..." -PercentComplete 0 $recipients = Get-Recipient -ResultSize Unlimited | Where-Object { $_.RecipientTypeDetails -ne 'GuestMailUser' } Write-Progress -Activity "Processing aliases" -Status "Recipient list loaded." -PercentComplete 5 } else { $normalizedDomain = $Domain.Trim().ToLowerInvariant().TrimStart('@') if ([string]::IsNullOrWhiteSpace($normalizedDomain)) { Write-NCMessage "Domain filter cannot be empty." -Level ERROR return } Write-Progress -Activity "Processing aliases" -Status "Retrieving recipient list for domain '$normalizedDomain' ..." -PercentComplete 0 $recipients = Get-Recipient -ResultSize Unlimited | Where-Object { $_.RecipientTypeDetails -ne 'GuestMailUser' -and $_.EmailAddresses -like "*@$normalizedDomain" } Write-Progress -Activity "Processing aliases" -Status "Recipient list loaded." -PercentComplete 5 } } $willExport = $Csv -or $PSCmdlet.ParameterSetName -ne 'Single' if ($willExport -and -not $resolvedCsvFolder) { try { $resolvedCsvFolder = Test-Folder $CsvFolder } catch { Write-NCMessage "Invalid CSV folder. $($_.Exception.Message)" -Level ERROR return } } $recipientCount = @($recipients).Count $addressCount = 0 foreach ($recipient in $recipients) { $addressCount += @($recipient.EmailAddresses).Count } $baseProgress = if ($PSCmdlet.ParameterSetName -eq 'Single') { 0 } else { 5 } $progressSpan = if ($willExport) { 94 } else { 95 } $totalWorkUnits = [Math]::Max($recipientCount + $addressCount, 1) $completedWorkUnits = 0 $recipientIndex = 0 $normalizeText = { param($value) return Get-NormalizedText -Value $value } $buildAliasKey = { param($row) $primary = & $normalizeText $row.PrimarySmtpAddress $alias = & $normalizeText $row.Alias $isPrimary = if ($null -ne $row.IsPrimary) { [string]$row.IsPrimary } else { $null } $isMoera = if ($null -ne $row.IsMoera) { [string]$row.IsMoera } else { $null } return "{0}|{1}|{2}|{3}" -f $primary, $alias, $isPrimary, $isMoera } $existingAliasKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $csvPathResolved = $null if ($willExport) { $folderForExport = if ($resolvedCsvFolder) { $resolvedCsvFolder } else { Test-Folder $CsvFolder } $defaultCsvPath = New-File "$folderForExport\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-Alias-Report.csv" $csvPathResolved = $defaultCsvPath if ($Resume) { $resumePath = $null if (-not [string]::IsNullOrWhiteSpace($CsvPath)) { $resumePath = $CsvPath } else { $existingCsv = Get-ChildItem -LiteralPath $folderForExport -File -Filter "*_M365-Alias-Report.csv" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if ($existingCsv) { $resumePath = $existingCsv.FullName } } if ($resumePath) { $csvPathResolved = $resumePath if (Test-Path -LiteralPath $csvPathResolved) { try { foreach ($row in (Import-CSV -LiteralPath $csvPathResolved -Delimiter $NCVars.CSV_DefaultLimiter -ErrorAction Stop)) { $null = $existingAliasKeys.Add((& $buildAliasKey $row)) } Write-NCMessage ("Resuming alias export from {0}; {1} row(s) already recorded." -f $csvPathResolved, $existingAliasKeys.Count) -Level INFO } catch { Write-NCMessage ("Unable to read existing CSV '{0}' for resume. {1}" -f $csvPathResolved, $_.Exception.Message) -Level WARNING $existingAliasKeys.Clear() $csvPathResolved = $defaultCsvPath } } else { Write-NCMessage ("Resume requested for '{0}', but the file does not exist. Starting a new report at that path." -f $csvPathResolved) -Level INFO } } else { Write-NCMessage ("Resume requested, but no existing CSV was found. Starting a new report at {0}." -f $csvPathResolved) -Level INFO } } Write-NCMessage ("Alias export will flush every {0} recipient(s). Resume: {1}. Stop after {2} consecutive error(s)." -f $BatchSize, $Resume.IsPresent, $MaxConsecutiveErrors) -Level INFO Write-NCMessage "Saving report to $csvPathResolved" -Level DEBUG } $updateAliasProgress = { param( [string]$Status, [switch]$Advance ) if ($Advance) { $completedWorkUnits++ } $percentage = [Math]::Min(99, [Math]::Round($baseProgress + (($completedWorkUnits / $totalWorkUnits) * $progressSpan), 2)) Write-Progress -Activity "Processing aliases" -Status $Status -PercentComplete $percentage } foreach ($recipient in $recipients) { $recipientIndex++ & $updateAliasProgress -Status "Recipient $recipientIndex of $recipientCount" -Advance $rows = [System.Collections.Generic.List[object]]::new() foreach ($address in $recipient.EmailAddresses) { & $updateAliasProgress -Status "Recipient $recipientIndex of $recipientCount" -Advance if ($address -clike 'SMTP:*') { if ($willExport) { $rows.Add([pscustomobject]@{ DisplayName = $recipient.DisplayName Name = $recipient.Name UserPrincipalName = $recipient.PrimarySmtpAddress PrimarySmtpAddress = $recipient.PrimarySmtpAddress Alias = $address.Replace('SMTP:', '') IsPrimary = $true IsMoera = $address.Replace('SMTP:', '') -match '@[^@]+\.onmicrosoft\.com$' }) | Out-Null } else { $rows.Add([pscustomobject]@{ PrimarySmtpAddress = $recipient.PrimarySmtpAddress Alias = $address.Replace('SMTP:', '') IsPrimary = $true }) | Out-Null } } elseif ($address -clike 'smtp:*') { if ($willExport) { $rows.Add([pscustomobject]@{ DisplayName = $recipient.DisplayName Name = $recipient.Name UserPrincipalName = $recipient.PrimarySmtpAddress PrimarySmtpAddress = $recipient.PrimarySmtpAddress Alias = $address.Replace('smtp:', '') IsPrimary = $false IsMoera = $address.Replace('smtp:', '') -match '@[^@]+\.onmicrosoft\.com$' }) | Out-Null } else { $rows.Add([pscustomobject]@{ PrimarySmtpAddress = $recipient.PrimarySmtpAddress Alias = $address.Replace('smtp:', '') IsPrimary = $false }) | Out-Null } } } if ($rows.Count -eq 0) { Write-NCMessage "No aliases found for '$($recipient.PrimarySmtpAddress)'." -Level WARNING continue } if ($willExport) { $exportRows = $rows if (-not $IncludeMoera) { $exportRows = @($exportRows | Where-Object { -not $_.IsMoera }) } if ($exportRows.Count -eq 0) { continue } if (-not $IncludePrimaryOnly -and $exportRows.Count -eq 1 -and $exportRows[0].IsPrimary -and $exportRows[0].Alias -eq $exportRows[0].PrimarySmtpAddress) { continue } foreach ($row in $exportRows) { $rowKey = & $buildAliasKey $row if ($Resume -and $existingAliasKeys.Contains($rowKey)) { continue } $aliasRows.Add($row) | Out-Null } $processedSinceFlush++ if ($aliasRows.Count -gt 0 -and $processedSinceFlush -ge $BatchSize) { $aliasRows | Select-Object DisplayName, Name, UserPrincipalName, PrimarySmtpAddress, Alias, IsPrimary, IsMoera | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter -Append $aliasRows.Clear() $processedSinceFlush = 0 } } else { $aliasRows.AddRange($rows) } if ($MaxConsecutiveErrors -gt 0 -and $consecutiveErrors -ge $MaxConsecutiveErrors) { $aborted = $true break } } if ($aliasRows.Count -eq 0) { if ($willExport -and -not $IncludePrimaryOnly) { if ((Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) { Write-NCMessage "No new aliases found. Existing CSV at $csvPathResolved already contains the requested rows." -Level INFO } else { Write-NCMessage "No secondary aliases found. Use -IncludePrimaryOnly to include recipients that only have a primary SMTP address." -Level WARNING } } return } if ($willExport) { & $updateAliasProgress -Status "Writing CSV export ..." -Advance $exportAliases = $aliasRows | Sort-Object -Property PrimarySmtpAddress, @{ Expression = 'IsPrimary'; Descending = $true }, Alias if ($exportAliases.Count -eq 0) { Write-NCMessage "No exportable aliases found. Use -IncludeMoera to include MOERA addresses or -IncludePrimaryOnly to include recipients with only a primary SMTP address." -Level WARNING return } $finalExport = $exportAliases | Select-Object DisplayName, Name, UserPrincipalName, PrimarySmtpAddress, Alias, IsPrimary, IsMoera if ((Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) { $finalExport | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter -Append } else { $finalExport | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter } if ($aborted) { Write-NCMessage "Alias report export stopped early. Partial data kept at $csvPathResolved." -Level ERROR } else { Write-NCMessage "Alias report exported to $csvPathResolved" -Level SUCCESS } } else { $aliasRows | Sort-Object -Property PrimarySmtpAddress, @{ Expression = 'IsPrimary'; Descending = $true }, Alias } } end { Write-Progress -Activity "Processing aliases" -Completed Restore-ProgressAndInfoPreferences } } function Get-MboxPrimarySmtpAddress { <# .SYNOPSIS Returns the PrimarySmtpAddress for a mailbox or recipient. .DESCRIPTION Ensures Exchange Online connectivity and resolves the recipient to its PrimarySmtpAddress. .PARAMETER SourceMailbox Mailbox or recipient identity. Accepts pipeline input. .PARAMETER Raw Return only the PrimarySmtpAddress values. .PARAMETER Detailed Return additional recipient fields (Identity and RecipientTypeDetails). .EXAMPLE Get-MboxPrimarySmtpAddress -SourceMailbox user@contoso.com .EXAMPLE Get-MboxPrimarySmtpAddress -SourceMailbox user@contoso.com -Detailed #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string[]]$SourceMailbox, [switch]$Raw, [switch]$Detailed ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } foreach ($entry in $SourceMailbox) { try { $recipient = Get-Recipient -Identity $entry -ErrorAction Stop } catch { Write-NCMessage "Recipient '$entry' not available or not found. $($_.Exception.Message)" -Level ERROR continue } if ($Raw) { $recipient.PrimarySmtpAddress } else { if ($Detailed) { [pscustomobject]@{ DisplayName = $recipient.DisplayName PrimarySmtpAddress = $recipient.PrimarySmtpAddress Identity = $recipient.Identity RecipientTypeDetails = $recipient.RecipientTypeDetails } } else { [pscustomobject]@{ DisplayName = $recipient.DisplayName PrimarySmtpAddress = $recipient.PrimarySmtpAddress } } } } } end { Restore-ProgressAndInfoPreferences } } Set-Alias -Name gpa -Value Get-MboxPrimarySmtpAddress -Description "Get Primary Address (function)" function Get-MboxPermission { <# .SYNOPSIS Retrieves mailbox permissions for a single mailbox. .DESCRIPTION Shows FullAccess, SendAs, and SendOnBehalfTo permissions with optional summary counts. .PARAMETER SourceMailbox Mailbox identity to inspect. .PARAMETER IncludeSummary Display a short summary of counts. .EXAMPLE Get-MboxPermission -SourceMailbox user@contoso.com -IncludeSummary #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string]$SourceMailbox, [switch]$IncludeSummary ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } try { $mailbox = Get-Mailbox -Identity $SourceMailbox -ErrorAction Stop } catch { Write-NCMessage "Mailbox '$SourceMailbox' not found." -Level ERROR return } $results = [System.Collections.Generic.List[object]]::new() $fullAccessCount = 0 $sendAsCount = 0 $sendOnBehalfToCount = 0 $addAccessRight = { param( [psobject]$Entry, [string]$Right ) if (-not $Entry -or [string]::IsNullOrWhiteSpace($Right)) { return } $existingRights = @($Entry.AccessRights -split '\s*,\s*' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($existingRights -notcontains $Right) { $Entry.AccessRights = (@($existingRights + $Right) | Select-Object -Unique) -join ', ' } } Get-MailboxPermission -Identity $mailbox.PrimarySmtpAddress -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -eq 'FullAccess' -and -not $_.IsInherited } | ForEach-Object { $userMailbox = $_.User.ToString() $primary = (Get-Mailbox -Identity $userMailbox -ErrorAction SilentlyContinue).PrimarySmtpAddress $display = (Get-User -Identity $userMailbox -ErrorAction SilentlyContinue).DisplayName if ($primary) { $existing = $results | Where-Object { $_.UserMailbox -eq $primary } | Select-Object -First 1 if ($existing) { & $addAccessRight $existing 'FullAccess' } else { $results.Add([pscustomobject]@{ User = $display UserMailbox = $primary AccessRights = 'FullAccess' }) | Out-Null } $fullAccessCount++ } } Write-Progress -Activity "Gathered FullAccess permissions for $($mailbox.PrimarySmtpAddress) ..." -Status "35% Complete" -PercentComplete 35 Get-RecipientPermission -Identity $mailbox.PrimarySmtpAddress -AccessRights SendAs -ErrorAction SilentlyContinue | Where-Object { $_.Trustee.ToString() -ne 'NT AUTHORITY\SELF' -and $_.Trustee.ToString() -notlike 'S-1-5*' } | ForEach-Object { $userMailbox = $_.Trustee.ToString() $primary = (Get-Mailbox -Identity $userMailbox -ErrorAction SilentlyContinue).PrimarySmtpAddress $display = (Get-User -Identity $userMailbox -ErrorAction SilentlyContinue).DisplayName if ($primary) { $existing = $results | Where-Object { $_.UserMailbox -eq $primary } | Select-Object -First 1 if ($existing) { & $addAccessRight $existing 'SendAs' } else { $results.Add([pscustomobject]@{ User = $display UserMailbox = $primary AccessRights = 'SendAs' }) | Out-Null } $sendAsCount++ } } Write-Progress -Activity "Gathered SendAs permissions for $($mailbox.PrimarySmtpAddress) ..." -Status "50% Complete" -PercentComplete 50 foreach ($userMailbox in $mailbox.GrantSendOnBehalfTo) { $primary = (Get-Mailbox -Identity $userMailbox -ErrorAction SilentlyContinue).PrimarySmtpAddress $display = (Get-User -Identity $userMailbox -ErrorAction SilentlyContinue).DisplayName if ($primary) { $existing = $results | Where-Object { $_.UserMailbox -eq $primary } | Select-Object -First 1 if ($existing) { & $addAccessRight $existing 'SendOnBehalfTo' } else { $results.Add([pscustomobject]@{ User = $display UserMailbox = $primary AccessRights = 'SendOnBehalfTo' }) | Out-Null } $sendOnBehalfToCount++ } } Write-Progress -Activity "Gathered SendOnBehalfTo permissions for $($mailbox.PrimarySmtpAddress) ..." -Status "90% Complete" -PercentComplete 90 Add-EmptyLine Write-NCMessage ("Access Rights on {0} ({1})" -f $mailbox.DisplayName, $mailbox.PrimarySmtpAddress) -Level WARNING if ($PSCmdlet.MyInvocation.PipelineLength -gt 1) { $results } else { $results | Format-Table -AutoSize -Wrap } if ($IncludeSummary) { Add-EmptyLine Write-NCMessage "Summary of Permissions Found:" -Level INFO Write-NCMessage ("FullAccess: {0}" -f $fullAccessCount) -Level SUCCESS Write-NCMessage ("SendAs: {0}" -f $sendAsCount) -Level SUCCESS Write-NCMessage ("SendOnBehalfTo: {0}" -f $sendOnBehalfToCount) -Level SUCCESS } } end { Restore-ProgressAndInfoPreferences } } function Get-UserLastSeen { <# .SYNOPSIS Returns the most recent activity timestamps for a user mailbox. .DESCRIPTION Combines Exchange Online LastUserActionTime with Entra ID (Microsoft Graph) interactive sign-in logs. Falls back to mailbox activity if Graph sign-in logs are unavailable or permissions are missing. .PARAMETER User Mailbox identity (UPN, SMTP address, alias). Accepts pipeline input. .EXAMPLE Get-UserLastSeen -User user@contoso.com #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity', 'UserPrincipalName')] [string]$User ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } try { $mailbox = Get-Mailbox -Identity $User -ErrorAction Stop } catch { Write-NCMessage "Mailbox '$User' not found or not accessible. $($_.Exception.Message)" -Level ERROR return } $mailboxIdentity = $mailbox.PrimarySmtpAddress $lastMailboxAction = $null $lastSignIn = $null $stats = Get-MailboxStatisticsSafe -Identity $mailboxIdentity if ($stats) { $lastMailboxAction = $stats.LastUserActionTime } $graphReady = Test-MgGraphConnection -Scopes @('AuditLog.Read.All', 'Directory.Read.All') -EnsureExchangeOnline:$false if ($graphReady) { try { $signIns = Get-MgAuditLogSignIn -Filter "userId eq '$($mailbox.ExternalDirectoryObjectId)'" -All:$true -Top 20 -ErrorAction Stop if ($signIns) { $lastSignIn = $signIns | Sort-Object -Property CreatedDateTime -Descending | Select-Object -First 1 -ExpandProperty CreatedDateTime } } catch { Write-NCMessage ("Unable to retrieve sign-in logs for {0}. {1}" -f $mailboxIdentity, $_.Exception.Message) -Level WARNING } } else { Write-NCMessage "Microsoft Graph AuditLog.Read.All is not available. Returning mailbox activity only." -Level WARNING } $sources = @() if ($lastMailboxAction) { $sources += 'MailboxAction' } if ($lastSignIn) { $sources += 'SignInLog' } $lastSeen = $null if ($sources.Count -gt 0) { $timestamps = @() if ($lastMailboxAction) { $timestamps += $lastMailboxAction } if ($lastSignIn) { $timestamps += $lastSignIn } $lastSeen = $timestamps | Sort-Object -Descending | Select-Object -First 1 } [pscustomobject][ordered]@{ DisplayName = $mailbox.DisplayName PrimarySmtpAddress = $mailboxIdentity LastUserActionTime = $lastMailboxAction LastInteractiveSignIn = if ($lastSignIn) { [datetime]$lastSignIn } else { $null } LastSeen = if ($lastSeen) { [datetime]$lastSeen } else { $null } Source = if ($sources.Count -gt 0) { $sources -join ',' } else { 'None' } } } end { Restore-ProgressAndInfoPreferences } } function Get-MboxLastMessageTrace { <# .SYNOPSIS Returns the most recent received and sent message traces for a mailbox. .DESCRIPTION Resolves the mailbox to its PrimarySmtpAddress and queries Get-MessageTraceV2 for the latest received and sent messages, returning the trace objects and their timestamps. .PARAMETER SourceMailbox Mailbox or recipient identity (UPN, SMTP address, alias). Accepts pipeline input. .PARAMETER IncludeTrace Include raw message trace objects in the output. .EXAMPLE Get-MboxLastMessageTrace -SourceMailbox user@contoso.com #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity', 'UserPrincipalName', 'Mailbox', 'UPN')] [string]$SourceMailbox, [switch]$IncludeTrace ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } try { $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop } catch { Write-NCMessage "Mailbox or recipient '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR return } $primaryAddress = $recipient.PrimarySmtpAddress $receivedTrace = $null $sentTrace = $null try { $receivedTrace = Get-MessageTraceV2 -RecipientAddress $primaryAddress -ErrorAction Stop | Sort-Object Received -Descending | Select-Object -First 1 } catch { Write-NCMessage ("Unable to retrieve received message trace for '{0}'. {1}" -f $primaryAddress, $_.Exception.Message) -Level WARNING } try { $sentTrace = Get-MessageTraceV2 -SenderAddress $primaryAddress -ErrorAction Stop | Sort-Object Received -Descending | Select-Object -First 1 } catch { Write-NCMessage ("Unable to retrieve sent message trace for '{0}'. {1}" -f $primaryAddress, $_.Exception.Message) -Level WARNING } $result = [ordered]@{ DisplayName = $recipient.DisplayName PrimarySmtpAddress = $primaryAddress LastReceived = if ($receivedTrace) { $receivedTrace.Received } else { $null } LastReceivedSubject = if ($receivedTrace) { $receivedTrace.Subject } else { $null } LastReceivedSender = if ($receivedTrace) { $receivedTrace.SenderAddress } else { $null } LastReceivedRecipient = if ($receivedTrace) { $receivedTrace.RecipientAddress } else { $null } LastReceivedStatus = if ($receivedTrace) { $receivedTrace.Status } else { $null } LastSent = if ($sentTrace) { $sentTrace.Received } else { $null } LastSentSubject = if ($sentTrace) { $sentTrace.Subject } else { $null } LastSentSender = if ($sentTrace) { $sentTrace.SenderAddress } else { $null } LastSentRecipient = if ($sentTrace) { $sentTrace.RecipientAddress } else { $null } LastSentStatus = if ($sentTrace) { $sentTrace.Status } else { $null } } if ($IncludeTrace.IsPresent) { $result.LastReceivedTrace = $receivedTrace $result.LastSentTrace = $sentTrace } [pscustomobject]$result } end { Restore-ProgressAndInfoPreferences } } function New-SharedMailbox { <# .SYNOPSIS Creates a shared mailbox with opinionated defaults. .DESCRIPTION Ensures Exchange Online connectivity, creates the shared mailbox, enables copies of sent messages, and sets deleted items retention. .PARAMETER SharedMailboxSMTPAddress Primary SMTP address for the new shared mailbox. .PARAMETER SharedMailboxDisplayName Display name for the mailbox. .PARAMETER SharedMailboxAlias Alias for the mailbox. .EXAMPLE New-SharedMailbox -SharedMailboxSMTPAddress user@contoso.com -SharedMailboxDisplayName "Contoso - Info" -SharedMailboxAlias contoso_info #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SharedMailboxSMTPAddress, [Parameter(Mandatory)] [string]$SharedMailboxDisplayName, [Parameter(Mandatory)] [string]$SharedMailboxAlias ) if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } try { New-Mailbox -Name $SharedMailboxDisplayName -Alias $SharedMailboxAlias -Shared -PrimarySmtpAddress $SharedMailboxSMTPAddress -ErrorAction Stop Write-NCMessage ("`nSet outgoing e-mail copy save for {0}" -f $SharedMailboxSMTPAddress) -Level INFO Set-Mailbox -Identity $SharedMailboxSMTPAddress -MessageCopyForSentAsEnabled $true Set-Mailbox -Identity $SharedMailboxSMTPAddress -MessageCopyForSendOnBehalfEnabled $true Set-Mailbox -Identity $SharedMailboxSMTPAddress -RetainDeletedItemsFor 30 Write-NCMessage "All done, remember to set access and editing rights to the new mailbox." -Level SUCCESS } catch { Write-NCMessage "Unable to create shared mailbox. $($_.Exception.Message)" -Level ERROR } } function Remove-MboxAlias { <# .SYNOPSIS Removes an alias from a mailbox or mail-enabled recipient. .DESCRIPTION Validates Exchange Online connectivity, resolves the recipient, and removes the specified alias. .PARAMETER SourceMailbox Mailbox or recipient identity. Accepts pipeline input. .PARAMETER MailboxAlias Alias to remove (SMTP address). .EXAMPLE Remove-MboxAlias -SourceMailbox user@contoso.com -MailboxAlias alias@contoso.com #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string]$SourceMailbox, [Parameter(Mandatory)] [string]$MailboxAlias ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } $normalizedAlias = $MailboxAlias.Trim().ToLowerInvariant() function Test-ProxyAddressPresent { param( [Parameter(Mandatory)] [object[]]$EmailAddresses, [Parameter(Mandatory)] [string]$TargetAddress ) foreach ($proxy in $EmailAddresses) { $proxyText = $proxy.ToString() $separatorIndex = $proxyText.IndexOf(':') $addressPart = if ($separatorIndex -ge 0) { $proxyText.Substring($separatorIndex + 1) } else { $proxyText } if ($addressPart.Trim().ToLowerInvariant() -eq $TargetAddress) { return $true } } return $false } try { $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop } catch { Write-NCMessage "Mailbox or recipient '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR return } try { switch ($recipient.RecipientTypeDetails) { 'MailContact' { Set-MailContact -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop } 'MailUser' { Set-MailUser -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop } { ($_ -eq 'MailUniversalDistributionGroup') -or ($_ -eq 'DynamicDistributionGroup') -or ($_ -eq 'MailUniversalSecurityGroup') } { Set-DistributionGroup -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop } default { Set-Mailbox -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop } } } catch { Write-NCMessage "Unable to remove alias '$MailboxAlias' from '$($recipient.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR return } try { $updatedRecipient = Get-Recipient -Identity $recipient.Identity -ErrorAction Stop } catch { Write-NCMessage "Alias removal attempted, but unable to validate final state for '$($recipient.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level WARNING return } if (Test-ProxyAddressPresent -EmailAddresses $updatedRecipient.EmailAddresses -TargetAddress $normalizedAlias) { Write-NCMessage ("Alias '{0}' is still present on {1}. It may be protected (for example, used as WindowsLiveId)." -f $MailboxAlias, $updatedRecipient.PrimarySmtpAddress) -Level WARNING } else { Write-NCMessage ("Alias '{0}' removed from {1}." -f $MailboxAlias, $updatedRecipient.PrimarySmtpAddress) -Level SUCCESS } Get-MboxAlias -SourceMailbox $updatedRecipient.PrimarySmtpAddress } end { Restore-ProgressAndInfoPreferences } } function Remove-MboxPermission { <# .SYNOPSIS Removes mailbox permissions from users. .DESCRIPTION Ensures Exchange Online connectivity, validates target and user mailboxes, and removes FullAccess, SendAs, SendOnBehalfTo, or all of them. .PARAMETER SourceMailbox Mailbox identity to update. .PARAMETER UserMailbox One or more users to remove permissions from. Accepts pipeline input. .PARAMETER AccessRights Permission type to remove: All, FullAccess, SendAs, SendOnBehalfTo. Defaults to All. .PARAMETER ClearAll Removes any non-inherited FullAccess, SendAs, and SendOnBehalfTo permissions for the source mailbox. .EXAMPLE Remove-MboxPermission -SourceMailbox user@contoso.com -UserMailbox user@contoso.com -AccessRights SendAs .EXAMPLE Remove-MboxPermission -SourceMailbox user@contoso.com -ClearAll #> [CmdletBinding(DefaultParameterSetName = 'User')] param( [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'User')] [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'All')] [Alias('Identity')] [string]$SourceMailbox, [Parameter(Mandatory, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'User')] [string[]]$UserMailbox, [Parameter(ParameterSetName = 'User')] [ValidateSet('All', 'FullAccess', 'SendAs', 'SendOnBehalfTo')] [string]$AccessRights = 'All', [Parameter(Mandatory, ParameterSetName = 'All')] [switch]$ClearAll ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } try { $targetMailbox = Get-Mailbox -Identity $SourceMailbox -ErrorAction Stop } catch { Write-NCMessage "Mailbox '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR return } if ($PSCmdlet.ParameterSetName -eq 'All') { $targetAddress = $targetMailbox.PrimarySmtpAddress $fullAccessUsers = Get-MailboxPermission -Identity $targetAddress -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -eq 'FullAccess' -and -not $_.IsInherited -and $_.User.ToString() -ne 'NT AUTHORITY\SELF' -and $_.User.ToString() -notlike 'S-1-5*' } | ForEach-Object { $_.User.ToString() } | Sort-Object -Unique foreach ($userIdentity in $fullAccessUsers) { Write-NCMessage ("Removing FullAccess for {0} from {1} ..." -f $userIdentity, $targetAddress) -Level INFO Remove-MailboxPermission -Identity $targetAddress -User $userIdentity -AccessRights FullAccess -Confirm:$false } $sendAsUsers = Get-RecipientPermission -Identity $targetAddress -AccessRights SendAs -ErrorAction SilentlyContinue | Where-Object { $_.Trustee.ToString() -ne 'NT AUTHORITY\SELF' -and $_.Trustee.ToString() -notlike 'S-1-5*' } | ForEach-Object { $_.Trustee.ToString() } | Sort-Object -Unique foreach ($userIdentity in $sendAsUsers) { Write-NCMessage ("Removing SendAs for {0} from {1} ..." -f $userIdentity, $targetAddress) -Level INFO Remove-RecipientPermission -Identity $targetAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false } $sendOnBehalfUsers = @($targetMailbox.GrantSendOnBehalfTo) | ForEach-Object { $_.ToString() } | Sort-Object -Unique foreach ($userIdentity in $sendOnBehalfUsers) { Write-NCMessage ("Removing SendOnBehalfTo for {0} from {1} ..." -f $userIdentity, $targetAddress) -Level INFO Set-Mailbox -Identity $targetAddress -GrantSendOnBehalfTo @{ remove = $userIdentity } -Confirm:$false | Out-Null } } else { foreach ($user in $UserMailbox) { try { $userObject = Get-User -Identity $user -ErrorAction Stop } catch { Add-EmptyLine Write-NCMessage "The mailbox '$user' does not exist. Please check the provided e-mail address." -Level ERROR continue } $userIdentity = $userObject.UserPrincipalName switch ($AccessRights) { 'FullAccess' { Write-NCMessage ("Removing FullAccess for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO Remove-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -Confirm:$false } 'SendAs' { Write-NCMessage ("Removing SendAs for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO Remove-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false } 'SendOnBehalfTo' { Write-NCMessage ("Removing SendOnBehalfTo for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO Set-Mailbox -Identity $targetMailbox.PrimarySmtpAddress -GrantSendOnBehalfTo @{ remove = $userIdentity } -Confirm:$false | Out-Null } 'All' { Write-NCMessage ("Removing FullAccess for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO Remove-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -Confirm:$false Write-NCMessage ("Removing SendAs for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO Remove-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false Write-NCMessage ("Removing SendOnBehalfTo for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO Set-Mailbox -Identity $targetMailbox.PrimarySmtpAddress -GrantSendOnBehalfTo @{ remove = $userIdentity } -Confirm:$false | Out-Null } } } } } end { Restore-ProgressAndInfoPreferences } } function Set-MboxLanguage { <# .SYNOPSIS Sets mailbox regional language. .DESCRIPTION Changes language for a single mailbox or a list provided via CSV (EmailAddress column). .PARAMETER SourceMailbox Mailbox to update. Accepts pipeline input. Ignored when -Csv is provided. .PARAMETER Language Language tag (default it-IT). .PARAMETER Csv CSV file with EmailAddress column containing mailboxes to update. .EXAMPLE Set-MboxLanguage -SourceMailbox user@contoso.com -Language en-US .EXAMPLE Set-MboxLanguage -Csv C:\temp\mailboxes.csv -Language it-IT #> [CmdletBinding(DefaultParameterSetName = 'Mailbox')] param( [Parameter(ParameterSetName = 'Mailbox', Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string[]]$SourceMailbox, [Parameter()] [string]$Language = 'it-IT', [Parameter(ParameterSetName = 'Csv', Mandatory = $true)] [string]$Csv ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } $mailboxes = @() if ($PSCmdlet.ParameterSetName -eq 'Csv') { if (-not (Test-Path -LiteralPath $Csv)) { Write-NCMessage "CSV file '$Csv' not found." -Level ERROR return } try { $mailboxes = (Import-Csv -LiteralPath $Csv -Delimiter $NCVars.CSV_DefaultLimiter) | Where-Object { $_.EmailAddress } } catch { Write-NCMessage "Unable to read CSV file '$Csv'. $($_.Exception.Message)" -Level ERROR return } } else { $mailboxes = $SourceMailbox } $counter = 0 $total = $mailboxes.Count foreach ($entry in $mailboxes) { $address = if ($entry.PSObject.Properties.Match('EmailAddress')) { $entry.EmailAddress } else { $entry } if (-not $address) { continue } $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $total Write-Progress -Activity "Changing language to $Language" -Status "$counter of $total - $Percentage%" -PercentComplete $Percentage try { Set-MailboxRegionalConfiguration -Identity $address -LocalizeDefaultFolderName:$true -Language $Language -ErrorAction Stop $result = Get-MailboxRegionalConfiguration -Identity $address -ErrorAction Stop [pscustomobject]@{ PrimarySmtpAddress = $result.Identity Language = $result.Language TimeZone = $result.TimeZone } } catch { Write-NCMessage "Failed to update mailbox '$address'. $($_.Exception.Message)" -Level ERROR } } } end { Write-Progress -Activity "Changing mailbox language" -Completed Restore-ProgressAndInfoPreferences } } function Set-MboxRulesQuota { <# .SYNOPSIS Sets mailbox rules quota to 256KB. .DESCRIPTION Iterates the provided mailboxes, sets the RulesQuota to 256KB, and returns the updated values. .PARAMETER SourceMailbox Mailboxes to update. Accepts pipeline input. .EXAMPLE Set-MboxRulesQuota -SourceMailbox user1@contoso.com, user2@contoso.com #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string[]]$SourceMailbox ) begin { Set-ProgressAndInfoPreferences $results = [System.Collections.Generic.List[object]]::new() $counter = 0 } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } foreach ($mailbox in $SourceMailbox) { try { $recipient = Get-Recipient -Identity $mailbox -ErrorAction Stop } catch { Write-NCMessage "Mailbox '$mailbox' not found. $($_.Exception.Message)" -Level ERROR continue } $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $SourceMailbox.Count Write-Progress -Activity "Processing $($recipient.PrimarySmtpAddress)" -Status "$counter of $($SourceMailbox.Count) - $Percentage%" -PercentComplete $Percentage try { Set-Mailbox -Identity $recipient.PrimarySmtpAddress -RulesQuota 256KB $results.Add([pscustomobject]@{ PrimarySmtpAddress = $recipient.PrimarySmtpAddress 'Rules Quota' = (Get-Mailbox -Identity $recipient.PrimarySmtpAddress).RulesQuota }) | Out-Null } catch { Write-NCMessage $_.Exception.Message -Level ERROR } } } end { Write-Progress -Activity "Processing mailbox rules quota" -Completed Restore-ProgressAndInfoPreferences $results } } function Set-SharedMboxCopyForSent { <# .SYNOPSIS Enables sent-item copy options on shared mailboxes. .DESCRIPTION For each shared mailbox, enables MessageCopyForSentAsEnabled and MessageCopyForSendOnBehalfEnabled, returning the updated status and listing any errors. .PARAMETER SourceMailbox Shared mailboxes to update. Accepts pipeline input. .EXAMPLE Set-SharedMboxCopyForSent -SourceMailbox user@contoso.com #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Identity')] [string[]]$SourceMailbox ) begin { Set-ProgressAndInfoPreferences $results = [System.Collections.Generic.List[object]]::new() $errors = [System.Collections.Generic.List[string]]::new() $counter = 0 } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } foreach ($mailbox in $SourceMailbox) { try { $recipient = Get-Recipient -Identity $mailbox -ErrorAction Stop if ($recipient.RecipientTypeDetails -ne 'SharedMailbox') { $errors.Add("{0} is not a Shared Mailbox." -f $mailbox) | Out-Null continue } $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $SourceMailbox.Count Write-Progress -Activity "Processing $($recipient.PrimarySmtpAddress)" -Status "$counter of $($SourceMailbox.Count) - $Percentage%" -PercentComplete $Percentage Set-Mailbox -Identity $recipient.PrimarySmtpAddress -MessageCopyForSentAsEnabled $true Set-Mailbox -Identity $recipient.PrimarySmtpAddress -MessageCopyForSendOnBehalfEnabled $true $updated = Get-Mailbox -Identity $recipient.PrimarySmtpAddress $results.Add([pscustomobject]@{ PrimarySmtpAddress = $recipient.PrimarySmtpAddress 'Copy for SentAs' = $updated.MessageCopyForSentAsEnabled 'Copy for SendOnBehalf' = $updated.MessageCopyForSendOnBehalfEnabled }) | Out-Null } catch { Write-NCMessage $_.Exception.Message -Level ERROR } } } end { Write-Progress -Activity "Processing shared mailbox sent items copy" -Completed Restore-ProgressAndInfoPreferences $results if ($errors.Count -gt 0) { Write-NCMessage ($errors -join [Environment]::NewLine) -Level WARNING } } } function Test-SharedMailboxCompliance { <# .SYNOPSIS Reports shared mailbox sign-in activity and licensing. .DESCRIPTION Uses Microsoft Graph sign-in logs and assigned plans to flag shared mailboxes with successful sign-ins and missing licenses. .PARAMETER GridView Show the result in Out-GridView (default behaviour). Specify -GridView:$false to return objects. .EXAMPLE Test-SharedMailboxCompliance #> [CmdletBinding()] param( [switch]$GridView ) begin { Set-ProgressAndInfoPreferences } process { if (-not (Test-EOLConnection)) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR return } $graphConnected = Test-MgGraphConnection -Scopes @('AuditLog.Read.All', 'Directory.Read.All') -EnsureExchangeOnline:$false if (-not $graphConnected) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Graph modules. Please check logs." -Level ERROR return } Add-EmptyLine Write-NCMessage "Finding shared mailboxes ..." -NoNewline $mailboxes = Get-ExoMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited | Sort-Object DisplayName if (-not $mailboxes) { Write-NCMessage "No shared mailboxes found." -Level WARNING return } $mailboxLabel = if ($mailboxes.Count -eq 1) { 'shared mailbox found' } else { 'shared mailboxes found' } Write-NCMessage (" {0} {1}." -f $mailboxes.Count, $mailboxLabel) -Level SUCCESS $exoPlan1 = '9aaf7827-d63c-4b61-89c3-182f06f82e5c' $exoPlan2 = 'efb87545-963c-4e0d-99df-69c6916d9eb0' $report = [System.Collections.Generic.List[object]]::new() $counter = 0 foreach ($mbx in $mailboxes) { $counter++ $Percentage = Get-NCProgressPercent -Current $counter -Total $mailboxes.Count Write-Progress -Activity "Checking $($mbx.DisplayName)" -Status "$counter of $($mailboxes.Count) - $Percentage%" -PercentComplete $Percentage $logsFound = $false $exoPlan1Found = $false $exoPlan2Found = $false try { $signIns = Get-MgAuditLogSignIn -Filter "userid eq '$($mbx.ExternalDirectoryObjectId)'" -All -Top 20 -ErrorAction Stop if ($signIns) { foreach ($log in $signIns) { if ($log.Status.ErrorCode -eq 0) { $logsFound = $true break } } } } catch { Write-NCMessage ("Unable to retrieve sign-in records for {0}. {1}" -f $mbx.DisplayName, $_.Exception.Message) -Level ERROR } if ($logsFound) { Write-NCMessage ("Sign-in records found for shared mailbox {0}" -f $mbx.DisplayName) -Level WARNING try { $user = Get-MgUser -UserId $mbx.ExternalDirectoryObjectId -Property UserPrincipalName, assignedPlans $exoPlans = @($user.AssignedPlans | Where-Object { $_.Service -eq 'exchange' -and $_.capabilityStatus -eq 'Enabled' }) $exoPlan1Found = $exoPlan1 -in $exoPlans.ServicePlanId $exoPlan2Found = $exoPlan2 -in $exoPlans.ServicePlanId } catch { Write-NCMessage ("Unable to read license info for {0}. {1}" -f $mbx.DisplayName, $_.Exception.Message) -Level ERROR } } else { Write-NCMessage ("No successful sign-in records found for shared mailbox {0}" -f $mbx.DisplayName) -Level SUCCESS } $report.Add([pscustomobject]@{ DisplayName = $mbx.DisplayName ExternalDirectoryObjectId = $mbx.ExternalDirectoryObjectId 'Sign in Record Found' = if ($logsFound) { 'Yes' } else { 'No' } 'Exchange Online Plan 1' = $exoPlan1Found 'Exchange Online Plan 2' = $exoPlan2Found }) | Out-Null } Write-Progress -Activity "Checking shared mailboxes" -Completed $showGrid = if ($PSBoundParameters.ContainsKey('GridView')) { $GridView.IsPresent } else { $true } if ($showGrid) { $report | Out-GridView -Title "Shared Mailbox Sign-In Records and Licensing Status" } else { $report } } end { Restore-ProgressAndInfoPreferences } } |