Revisión de buzones compartidos
Lista todos los buzones compartidos con su tamaño, quién tiene FullAccess y SendAs, si tienen licencia y si su inicio de sesión está bloqueado. Señala los que no tienen delegados, los que permiten iniciar sesión y los que pasan de 50 GB.
Solo lecturaPide 1 permisoProbado con respuestas simuladas. Sin ejecutar en un tenant real.
Ejemplo en México
Empresa de logística en Monterrey, 120 personas
Hay un buzón compartido por sucursal y por cliente, creados durante años y con mucha rotación de personal. El auditor del corporativo pregunta quién entra a cada uno.
.\get-buzones-compartidos.ps1 -RutaCsv .\compartidos.csvObtiene la lista de delegados como evidencia de la revisión, bloquea el inicio de sesión en los que lo permitían y da de baja los que ya no tienen delegados.
Cómo implementarlo
- Instala los módulos si no los tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser e Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
- Ejecuta el script con -RutaCsv e inicia sesión en Microsoft Graph y en Exchange Online con una cuenta que tenga el rol indicado.
- Filtra la columna Alertas en el CSV.
- Bloquea el inicio de sesión donde siga permitido, asigna Exchange Online Plan 2 a los que pasan de 50 GB y decide con cada área qué hacer con los que no tienen delegados.
Ver el código completo de get-buzones-compartidos.ps1
<#
.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." }