Saltar al contenido
MS Scripts

Scripts de Exchange

Grupos de correo sin dueño o sin miembros

Encuentra listas de distribución y grupos de seguridad con correo que nadie administra o que están vacíos, con su número de miembros y si aceptan correo de fuera.

Solo lecturaSin permisos de GraphProbado con respuestas simuladas. Sin ejecutar en un tenant real.

Antes de ejecutarloCuenta solo miembros directos: un grupo que solo contiene otro grupo vacío no sale como vacío. Los grupos de Microsoft 365 y las listas de salas quedan fuera. Hace una consulta por grupo, así que tarda si tienes miles. Si un grupo falla, sale como "No se pudo consultar" y el script termina con error después de guardar el CSV.

Ejemplo en México

Maquiladora en Ciudad Juárez, 900 personas con tres turnos

Con la rotación de personal se acumularon listas por turno y por línea. Un proveedor sigue escribiendo a una lista de la que ya nadie sabe quién responde.

.\get-grupos-distribucion-sin-dueno.ps1 -RutaCsv .\grupos-sin-dueno.csv

Obtienen las listas sin dueño y las vacías. Asignan responsable a las que siguen en uso, borran las vacías y cierran al correo externo las que no lo necesitan.

Cómo implementarlo

  1. Instala el módulo si no lo tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
  2. Ejecuta el script con -RutaCsv.
  3. Empieza por los grupos con AceptaCorreoExterno en Sí: cualquiera de fuera puede escribirles.
  4. A cada grupo asígnale dueño con Set-DistributionGroup -ManagedBy o, si ya nadie lo usa, elimínalo tras confirmarlo con el área.
Ver el código completo de get-grupos-distribucion-sin-dueno.ps1
<#
.SYNOPSIS
    Encuentra listas de distribución y grupos de seguridad con correo que no tienen dueño o no tienen miembros.

.DESCRIPTION
    Un grupo sin dueño nadie lo mantiene, y uno sin miembros se traga el correo. El reporte da el número de miembros
    y si el grupo acepta correo de fuera de la organización. Cuenta solo miembros directos, sin expandir grupos anidados.
    Hace una consulta por grupo. 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-grupos-distribucion-sin-dueno.ps1 -RutaCsv .\grupos-sin-dueno.csv

.NOTES
    Permisos: ninguno de Graph; usa ExchangeOnlineManagement.
    Rol mínimo: Lector global (o View-Only Recipients en Exchange Online).
    Licencia: Exchange Online.
    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'
}

# 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 }

Connect-ExchangeOnline -ShowBanner:$false
try {
    # RoomList queda fuera: son listas de salas, no grupos de personas.
    $grupos = @(Get-DistributionGroup -ResultSize Unlimited -IncludeManagedByWithDisplayNames -RecipientTypeDetails MailUniversalDistributionGroup, MailUniversalSecurityGroup, MailNonUniversalGroup -ErrorAction Stop)
    $n = 0
    $filas = @(foreach ($g in $grupos) {
        $n++
        $correo = "$($g.PrimarySmtpAddress)"
        Write-Progress -Activity 'Revisando grupos' -Status $correo -PercentComplete ($n / $grupos.Count * 100)

        $duenos = @(@($g.ManagedByWithDisplayNames) | Where-Object { $_ } | ForEach-Object { "$_" })
        if (-not $duenos) { $duenos = @(@($g.ManagedBy) | Where-Object { $_ } | ForEach-Object { "$_" }) }

        $miembros = 'No se pudo consultar'
        $nota = ''
        # Sin identidad no se llama: el cmdlet interpretaría un valor vacío como todos los grupos.
        if ($correo) {
            try {
                $miembros = @(Get-DistributionGroupMember -Identity $correo -ResultSize Unlimited -ErrorAction Stop).Count
            } catch { $nota = "$_" }
        } else { $nota = 'El grupo no trae correo' }

        $problemas = @()
        if (-not $duenos) { $problemas += 'Sin dueño' }
        if ($miembros -is [int] -and $miembros -eq 0) { $problemas += 'Sin miembros' }
        if ($miembros -isnot [int]) { $problemas += 'No se pudo consultar' }
        if (-not $problemas) { continue }

        [pscustomobject]@{
            Nombre              = "$($g.DisplayName)"
            Correo              = $correo
            Tipo                = switch ("$($g.RecipientTypeDetails)") { 'MailUniversalSecurityGroup' { 'Seguridad con correo' } 'MailUniversalDistributionGroup' { 'Distribución' } 'MailNonUniversalGroup' { 'No universal' } default { "$_" } }
            Problema            = $problemas -join '. '
            Duenos              = $duenos -join '; '
            Miembros            = $miembros
            AceptaCorreoExterno = if ($g.RequireSenderAuthenticationEnabled -eq $false) { 'Sí' } elseif ($g.RequireSenderAuthenticationEnabled -eq $true) { 'No' } else { 'No determinado' }
            Nota                = $nota
        }
    })
    Write-Progress -Activity 'Revisando grupos' -Completed
} finally {
    Disconnect-ExchangeOnline -Confirm:$false
}

$fallas = @($filas | Where-Object { $_.Miembros -eq 'No se pudo consultar' })
Write-Host "Grupos revisados: $($grupos.Count). Sin dueño o sin miembros: $($filas.Count - $fallas.Count). Con falla: $($fallas.Count)"
if ($RutaCsv) {
    # Excel ejecutaría como fórmula un nombre que empiece con estos caracteres.
    foreach ($f in $filas) { foreach ($p in $f.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@\t\r]') { $p.Value = "'" + $p.Value } } }
    # UTF-8 con BOM para que Excel en español muestre bien los acentos.
    $filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM
    Write-Host "CSV guardado en $RutaCsv"
} else {
    $filas | Format-Table -AutoSize
}
if ($fallas) { throw "No se pudieron consultar los miembros de $($fallas.Count) grupos: $($fallas.Nombre -join ', ')" }

Relacionados