<# .SYNOPSIS Responde quién usa cada buzón compartido y cuáles están mal configurados. .DESCRIPTION Lista los buzones compartidos con tamaño, delegados con FullAccess y SendAs, licencia, si su inicio de sesión está bloqueado y si su tamaño exige licencia (más de 50 GB). Señala los que no tienen delegados y los que permiten inicio de sesión, porque Microsoft indica mantenerlo bloqueado. Si un buzón falla, queda en el reporte y el script termina con error al final, después de guardar el CSV. Solo lectura: no cambia nada en el tenant. .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .EXAMPLE .\get-buzones-compartidos.ps1 -RutaCsv .\compartidos.csv .NOTES Permisos: User.Read.All Rol mínimo: Lector global (View-Only Organization Management en Exchange Online). Licencia: Exchange Online Plan 1 o superior, incluido en Microsoft 365 Business Premium. Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> #Requires -Version 7.0 [CmdletBinding()] param( [string]$RutaCsv ) $ErrorActionPreference = 'Stop' if (-not (Get-Command Connect-ExchangeOnline -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module ExchangeOnlineManagement -Scope CurrentUser' } if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser' } # Los tamaños llegan como texto: 1.2 GB (1,288,490,188 bytes). Sin paréntesis (Unlimited o vacío) no hay bytes. function Get-Bytes($valor) { $texto = "$valor".Trim() if ($texto -match '\(([\d,. ]+)\s*bytes\)') { return [long]($Matches[1] -replace '\D') } if ($texto -match '^0(\.0+)?\s*B$') { return [long]0 } $null } function Limpiar-Fila($fila) { foreach ($p in $fila.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@\t\r]') { $p.Value = "'$($p.Value)" } } $fila } # El CSV se vacía desde el inicio: si algo truena o no hay resultados, no queda uno viejo que parezca actual. if ($RutaCsv) { Set-Content -LiteralPath $RutaCsv -Value $null -Encoding utf8BOM } $filas = [System.Collections.Generic.List[object]]::new() $fallas = [System.Collections.Generic.List[string]]::new() Connect-MgGraph -Scopes 'User.Read.All' -NoWelcome Connect-ExchangeOnline -ShowBanner:$false try { foreach ($b in @(Get-EXOMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited -ErrorAction Stop)) { try { # Sin id, la URI apuntaría a la lista completa de usuarios en lugar de a esta cuenta. if (-not $b.ExternalDirectoryObjectId) { throw 'El buzón no trae ExternalDirectoryObjectId' } $estadisticas = Get-EXOMailboxStatistics -Identity $b.UserPrincipalName -ErrorAction Stop $accesoTotal = @(Get-EXOMailboxPermission -Identity $b.UserPrincipalName -ResultSize Unlimited -ErrorAction Stop | Where-Object { "$($_.AccessRights)" -match 'FullAccess' -and $_.IsInherited -ne $true -and $_.Deny -ne $true -and "$($_.User)" -notlike 'NT AUTHORITY\*' } | ForEach-Object { "$($_.User)" }) $enviarComo = @(Get-EXORecipientPermission -Identity $b.UserPrincipalName -ResultSize Unlimited -ErrorAction Stop | Where-Object { "$($_.AccessRights)" -match 'SendAs' -and $_.AccessControlType -ne 'Deny' -and $_.IsInherited -ne $true -and "$($_.Trustee)" -notlike 'NT AUTHORITY\*' } | ForEach-Object { "$($_.Trustee)" }) $cuenta = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$($b.ExternalDirectoryObjectId)?`$select=accountEnabled,assignedLicenses" } catch { $fallas.Add("$($b.PrimarySmtpAddress)") $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName; Correo = $b.PrimarySmtpAddress; TamanoGB = $null; FullAccess = $null; SendAs = $null TieneLicencia = $null; InicioSesion = $null; TamanoExigeLicencia = $null; Alertas = "No se pudo consultar: $_" }) continue } $bytes = Get-Bytes $estadisticas.TotalItemSize $conLicencia = @($cuenta.assignedLicenses).Where({ $_ }).Count -gt 0 $exigeLicencia = $null -ne $bytes -and $bytes -gt 50GB $alertas = @( if (-not $accesoTotal -and -not $enviarComo) { 'Sin delegados' } if ($cuenta.accountEnabled -eq $true) { 'Permite inicio de sesión: bloquéalo' } if ($exigeLicencia -and -not $conLicencia) { 'Más de 50 GB sin licencia: asigna Exchange Online Plan 2' } if ($exigeLicencia -and $conLicencia) { 'Más de 50 GB: confirma que la licencia sea Exchange Online Plan 2' } ) $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName Correo = $b.PrimarySmtpAddress TamanoGB = if ($null -ne $bytes) { [math]::Round($bytes / 1GB, 2) } FullAccess = $accesoTotal -join '; ' SendAs = $enviarComo -join '; ' TieneLicencia = if ($conLicencia) { 'Sí' } else { 'No' } InicioSesion = switch ($cuenta.accountEnabled) { $true { 'Permitido' } $false { 'Bloqueado' } default { 'Sin dato' } } TamanoExigeLicencia = if ($null -eq $bytes) { 'Sin dato' } elseif ($exigeLicencia) { 'Sí' } else { 'No' } Alertas = $alertas -join '; ' }) } } finally { Disconnect-ExchangeOnline -Confirm:$false } $salida = @($filas | Sort-Object TamanoGB -Descending) Write-Host "Buzones compartidos: $($salida.Count). Con alguna alerta: $(@($salida | Where-Object Alertas).Count)." if ($RutaCsv) { if ($salida.Count) { # UTF-8 con BOM para que Excel en español muestre bien los acentos. $salida | ForEach-Object { Limpiar-Fila $_ } | Export-Csv -LiteralPath $RutaCsv -NoTypeInformation -Encoding utf8BOM } Write-Host "CSV guardado en $RutaCsv" } else { $salida | Format-Table -AutoSize } if ($fallas.Count) { throw "No se pudieron consultar $($fallas.Count) buzones: $($fallas -join ', '). Revisa las filas marcadas en el reporte." }