Saltar al contenido
MS Scripts

Scripts de Exchange

Salas y equipos: reservas y aprobadores

Lista salas y equipos con su capacidad, cómo se reservan y quién puede aprobar. Señala las solicitudes que se quedan sin respuesta porque nadie puede aprobarlas.

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

Antes de ejecutarloCon AutoUpdate en una sala los aprobadores no reciben las solicitudes, aunque aparezcan en QuienAprueba. Microsoft indica que no uses grupos anidados en estos ajustes. Si un recurso falla, sale como "No se pudo consultar" y el script termina con error después de guardar el CSV.

Ejemplo en México

Universidad privada en Puebla, 400 personas

Los profesores se quejan de que reservan la sala de consejo y nunca les confirman. Quien la aprobaba se jubiló el semestre pasado.

.\get-buzones-recursos.ps1 -TipoBuzon RoomMailbox -RutaCsv .\salas.csv

La sala de consejo aparece con QuienAprueba en Nadie y el aviso de que requiere aprobación. Asignan un nuevo aprobador y aprovechan para llenar la capacidad de las demás salas.

Cómo implementarlo

  1. Instala el módulo si no lo tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
  2. Ejecuta el script con -RutaCsv; usa -TipoBuzon RoomMailbox si solo quieres salas.
  3. Revisa la columna Problema: sala sin capacidad, calendario que no procesa o solicitudes sin aprobador.
  4. Corrige lo que encuentres con Set-CalendarProcessing y Set-Mailbox -ResourceCapacity, y corre de nuevo el script para confirmar.
Ver el código completo de get-buzones-recursos.ps1
<#
.SYNOPSIS
    Lista salas y equipos con su capacidad, cómo se reservan y quién puede aprobar.

.DESCRIPTION
    Por cada buzón de recurso lee la capacidad y la configuración de reservas automáticas, y señala lo que
    suele causar quejas: sala sin capacidad, reservas sin procesar o solicitudes que requieren aprobación sin nadie que apruebe.
    Hace una consulta por cada recurso. Solo lectura: no cambia nada en el tenant.

.PARAMETER TipoBuzon
    RoomMailbox para salas, EquipmentMailbox para equipos. Por defecto, ambos.

.PARAMETER RutaCsv
    Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.

.EXAMPLE
    .\get-buzones-recursos.ps1 -RutaCsv .\recursos.csv

.EXAMPLE
    .\get-buzones-recursos.ps1 -TipoBuzon RoomMailbox -RutaCsv .\salas.csv

.NOTES
    Permisos: ninguno de Graph; usa ExchangeOnlineManagement.
    Rol mínimo: Lector global (o View-Only Organization Management en Exchange Online).
    Licencia: Exchange Online. Los buzones de recurso no requieren licencia propia hasta 50 GB.
    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(
    [ValidateSet('RoomMailbox', 'EquipmentMailbox')][string[]]$TipoBuzon = @('RoomMailbox', 'EquipmentMailbox'),
    [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 {
    $recursos = @(Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails $TipoBuzon -Properties DisplayName, ResourceCapacity)
    $filas = @(foreach ($r in $recursos) {
        $correo = "$($r.PrimarySmtpAddress)"
        $tipo = switch ("$($r.RecipientTypeDetails)") { 'RoomMailbox' { 'Sala' } 'EquipmentMailbox' { 'Equipo' } default { "$_" } }
        $capacidad = if ($r.ResourceCapacity) { $r.ResourceCapacity } else { 'Sin definir' }
        $fila = [pscustomobject]@{ Nombre = "$($r.DisplayName)"; Correo = $correo; Tipo = $tipo; Capacidad = $capacidad; ReservaAutomatica = ''; QuienReserva = ''; QuienAprueba = ''; Problema = '' }
        $fila

        # Sin identidad no se llama: el cmdlet interpretaría un valor vacío como todos los buzones.
        if (-not $correo) { $fila.ReservaAutomatica = 'No se pudo consultar'; $fila.Problema = 'El buzón no trae correo'; continue }
        try {
            $c = Get-CalendarProcessing -Identity $correo -ErrorAction Stop
        } catch {
            $fila.ReservaAutomatica = 'No se pudo consultar'
            $fila.Problema = "$_"
            continue
        }

        $aprobadores = @($c.ResourceDelegates | Where-Object { $_ } | ForEach-Object { "$_" })
        $porLista = @($c.BookInPolicy | Where-Object { $_ } | ForEach-Object { "$_" })
        $conAprobacion = @($c.RequestInPolicy | Where-Object { $_ } | ForEach-Object { "$_" })
        $fila.ReservaAutomatica = switch ("$($c.AutomateProcessing)") {
            'AutoAccept' { 'Automática (AutoAccept)' }
            'AutoUpdate' { 'Solo aprobación manual (AutoUpdate)' }
            'None' { 'Desactivada (None)' }
            '' { 'No determinado' }
            default { "$_" }
        }
        $fila.QuienReserva = if ($c.AllBookInPolicy -eq $true) { 'Todos' }
            elseif ($c.AllRequestInPolicy -eq $true) { 'Todos, con aprobación' }
            elseif ($porLista -or $conAprobacion) { (@($porLista) + @($conAprobacion | ForEach-Object { "$_ (con aprobación)" })) -join '; ' }
            else { 'Nadie dentro de política' }
        $fila.QuienAprueba = if ($aprobadores) { $aprobadores -join '; ' } else { 'Nadie' }

        $problemas = @()
        if ($tipo -eq 'Sala' -and -not $r.ResourceCapacity) { $problemas += 'Sala sin capacidad definida' }
        if ("$($c.AutomateProcessing)" -eq 'None') { $problemas += 'El calendario no procesa solicitudes' }
        if ("$($c.AutomateProcessing)" -eq 'AutoUpdate' -and $tipo -eq 'Sala') { $problemas += 'AutoUpdate en una sala: los aprobadores no reciben las solicitudes' }
        if ("$($c.AutomateProcessing)" -eq 'AutoAccept' -and $c.AllBookInPolicy -ne $true -and ($c.AllRequestInPolicy -eq $true -or $conAprobacion) -and -not $aprobadores) { $problemas += 'Requiere aprobación y no hay quien apruebe' }
        if ($aprobadores -and $c.ForwardRequestsToDelegates -eq $false) { $problemas += 'Los aprobadores no reciben las solicitudes: ForwardRequestsToDelegates está apagado' }
        $fila.Problema = $problemas -join '. '
    })
} finally {
    Disconnect-ExchangeOnline -Confirm:$false
}

$fallas = @($filas | Where-Object { $_.ReservaAutomatica -eq 'No se pudo consultar' })
Write-Host "Recursos revisados: $($filas.Count). Con problema: $(@($filas | Where-Object { $_.Problema -and $_.ReservaAutomatica -ne 'No se pudo consultar' }).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 $($fallas.Count) recursos: $($fallas.Nombre -join ', ')" }

Relacionados