<# .SYNOPSIS Responde qué buzones están por llegar a su cuota de envío y cuánto espacio les falta. .DESCRIPTION Compara el tamaño de cada buzón con su cuota de envío real (ProhibitSendQuota, o la de la base de datos si el buzón usa UseDatabaseQuotaDefaults). Al llegar a esa cuota el buzón ya no puede enviar correo. Los buzones con cuota Unlimited no se pueden evaluar: quedan fuera y se cuentan en pantalla. 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 Porcentaje Porcentaje de la cuota a partir del cual se reporta un buzón. Por defecto, 80. .PARAMETER Tipo Tipo de buzón: UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox o Todos (por defecto). .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .EXAMPLE .\get-buzones-cerca-de-cuota.ps1 -Porcentaje 90 -Tipo UserMailbox -RutaCsv .\cuota.csv .NOTES Permisos: ninguno de Graph; usa ExchangeOnlineManagement. 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( [ValidateRange(1, 100)] [int]$Porcentaje = 80, [ValidateSet('UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox', 'Todos')] [string]$Tipo = 'Todos', [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' } # 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 } $tipos = if ($Tipo -eq 'Todos') { 'UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox' } else { $Tipo } $filas = [System.Collections.Generic.List[object]]::new() $fallas = [System.Collections.Generic.List[string]]::new() $ilimitados = 0 Connect-ExchangeOnline -ShowBanner:$false try { foreach ($b in @(Get-EXOMailbox -RecipientTypeDetails $tipos -ResultSize Unlimited -Properties ProhibitSendQuota, UseDatabaseQuotaDefaults -ErrorAction Stop)) { try { $estadisticas = Get-EXOMailboxStatistics -Identity $b.UserPrincipalName -Properties DatabaseProhibitSendQuota -ErrorAction Stop } catch { $fallas.Add("$($b.PrimarySmtpAddress)") $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName; Correo = $b.PrimarySmtpAddress; Tipo = $b.RecipientTypeDetails; TamanoGB = $null CuotaEnvioGB = $null; PorcentajeUsado = $null; FaltanGB = $null; Estado = "No se pudo consultar: $_" }) continue } # Con UseDatabaseQuotaDefaults, la cuota del buzón se ignora y aplica la de la base de datos. $cuota = if ($b.UseDatabaseQuotaDefaults -eq $true) { $estadisticas.DatabaseProhibitSendQuota } else { $b.ProhibitSendQuota } if ("$cuota".Trim() -eq 'Unlimited') { $ilimitados++; continue } $bytes = Get-Bytes $estadisticas.TotalItemSize $bytesCuota = Get-Bytes $cuota if ($null -eq $bytes -or -not $bytesCuota) { # Sin alguno de los dos valores no se puede saber si está cerca: se muestra en vez de darlo por bueno. $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName; Correo = $b.PrimarySmtpAddress; Tipo = $b.RecipientTypeDetails TamanoGB = if ($null -ne $bytes) { [math]::Round($bytes / 1GB, 2) } CuotaEnvioGB = if ($bytesCuota) { [math]::Round($bytesCuota / 1GB, 2) } PorcentajeUsado = $null; FaltanGB = $null; Estado = 'Sin dato de tamaño o de cuota' }) continue } $usado = $bytes / $bytesCuota * 100 if ($usado -lt $Porcentaje) { continue } $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName Correo = $b.PrimarySmtpAddress Tipo = $b.RecipientTypeDetails TamanoGB = [math]::Round($bytes / 1GB, 2) CuotaEnvioGB = [math]::Round($bytesCuota / 1GB, 2) PorcentajeUsado = [math]::Round($usado, 1) FaltanGB = [math]::Round([math]::Max($bytesCuota - $bytes, 0) / 1GB, 2) Estado = if ($usado -ge 100) { 'Ya no puede enviar correo' } else { 'Cerca de la cuota' } }) } } finally { Disconnect-ExchangeOnline -Confirm:$false } $salida = @($filas | Sort-Object PorcentajeUsado -Descending) Write-Host "Buzones en el reporte: $($salida.Count). Con cuota ilimitada, sin evaluar: $ilimitados." 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." }