Buzones cerca de su cuota de envío
Encuentra los buzones que están por llegar a la cuota con la que dejan de enviar correo, con la cuota real de cada uno y cuánto espacio les falta. Te avisa antes de que alguien se quede sin poder enviar.
Solo lecturaSin permisos de GraphProbado con respuestas simuladas. Sin ejecutar en un tenant real.
Ejemplo en México
Clínica privada en Mérida, 60 personas
Recepción dejó de poder enviar correo en plena semana de citas. Quieren saber quién sigue antes de que le pase al área de facturación.
.\get-buzones-cerca-de-cuota.ps1 -Porcentaje 85 -RutaCsv .\cuota.csvObtiene los buzones arriba de 85 % con los GB que les faltan y programa la limpieza de los dos más críticos esa misma semana.
Cómo implementarlo
- Instala el módulo si no lo tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
- Ejecuta el script con -Porcentaje (por defecto 80) y -RutaCsv, e inicia sesión con una cuenta que tenga el rol indicado.
- Revisa la pantalla: te dice cuántos buzones con cuota ilimitada quedaron sin evaluar.
- Abre el CSV ordenado por PorcentajeUsado y atiende primero los marcados "Ya no puede enviar correo": limpia, activa el archivado o cambia la licencia.
Ver el código completo de get-buzones-cerca-de-cuota.ps1
<#
.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." }