<# .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 ', ')" }