Archivo y retención por buzón
Muestra por buzón si tiene archivo en línea y de qué tamaño, retención por litigio, política de retención y bloqueos legales. Te dice qué correo se conserva antes de una auditoría o de tocar un buzón.
Solo lecturaSin permisos de GraphProbado con respuestas simuladas. Sin ejecutar en un tenant real.
Ejemplo en México
Clínica privada en Mérida, 120 personas
El corporativo pide evidencia de qué correo se conserva, y el área legal recuerda haber puesto a dos personas en retención por una demanda, sin saber si sigue activa.
.\get-archivado-y-retencion.ps1 -RutaCsv .\retencion.csvObtienen el CSV con cada buzón y sus retenciones. Confirman las dos retenciones por litigio y encuentran buzones grandes sin archivo. El archivo queda como apoyo para la auditoría.
Cómo implementarlo
- Instala el módulo si no lo tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
- Ejecuta el script con -RutaCsv; usa -TipoBuzon si solo te interesan los compartidos.
- Filtra en Excel por RetencionPorLitigio y BloqueoLegal para ver qué buzones no pueden purgarse.
- Revisa también PoliticasOrganizacion: esas políticas de Microsoft Purview aplican aunque BloqueoLegal diga Ninguno.
- Guarda el CSV: te ayuda a documentar qué se conservaba en la fecha de la revisión.
Ver el código completo de get-archivado-y-retencion.ps1
<#
.SYNOPSIS
Muestra por buzón qué se conserva: archivo en línea y su tamaño, retención por litigio, política de retención y bloqueos legales.
.DESCRIPTION
Sirve para auditorías y para saber qué correo está protegido contra borrado antes de tocar un buzón.
Traduce la propiedad InPlaceHolds (eDiscovery, In-Place Hold, políticas de Microsoft Purview) y cuenta las políticas
de Purview de toda la organización, que no aparecen en el buzón. Hace una consulta extra por cada archivo activo.
Solo lectura: no cambia nada en el tenant.
.PARAMETER TipoBuzon
Tipos de buzón a revisar. Por defecto, de usuario y compartidos.
.PARAMETER RutaCsv
Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.
.EXAMPLE
.\get-archivado-y-retencion.ps1 -RutaCsv .\retencion.csv
.EXAMPLE
.\get-archivado-y-retencion.ps1 -TipoBuzon SharedMailbox -RutaCsv .\retencion-compartidos.csv
.NOTES
Permisos: ninguno de Graph; usa ExchangeOnlineManagement.
Rol mínimo: Lector global (o View-Only Organization Management en Exchange Online).
Licencia: Exchange Online. La retención por litigio y el archivo en línea requieren Exchange Online Plan 2, Plan 1 con Exchange Online Archiving, o un plan que los incluya, como 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(
[ValidateSet('UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox')][string[]]$TipoBuzon = @('UserMailbox', 'SharedMailbox'),
[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 }
# En sesión remota el tamaño llega como texto: "1.2 GB (1,288,490,188 bytes)".
function ConvertTo-Gb {
param($Valor)
$texto = "$Valor".Trim()
if (-not $texto) { return $null }
if ($texto -eq 'Unlimited') { return 'Sin límite' }
if ($texto -match '\(([\d,.\s]+) bytes\)') { return [math]::Round([double]($Matches[1] -replace '\D', '') / 1GB, 2) }
if ($texto -match '^0(\.0+)?\s*B') { return [double]0 }
$null
}
$propiedades = 'DisplayName', 'ArchiveStatus', 'ArchiveGuid', 'ArchiveDatabase', 'LitigationHoldEnabled', 'LitigationHoldDate', 'LitigationHoldDuration', 'RetentionPolicy', 'InPlaceHolds', 'ComplianceTagHoldApplied', 'DelayHoldApplied'
Connect-ExchangeOnline -ShowBanner:$false
try {
# Las políticas de Purview de toda la organización no se ven en el buzón: solo sus exclusiones.
$politicasOrg = $null
$errorOrg = $null
try {
$politicasOrg = @((Get-OrganizationConfig -ErrorAction Stop).InPlaceHolds | Where-Object { "$_" -like 'mbx*' } | ForEach-Object { ("$_" -replace '^mbx', '') -replace ':.*$', '' })
} catch { $errorOrg = "$_" }
$buzones = @(Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails $TipoBuzon -Properties $propiedades)
$n = 0
$filas = @(foreach ($b in $buzones) {
$n++
$cuenta = if ($b.UserPrincipalName) { "$($b.UserPrincipalName)" } else { "$($b.PrimarySmtpAddress)" }
Write-Progress -Activity 'Revisando archivo y retención' -Status $cuenta -PercentComplete ($n / $buzones.Count * 100)
# Microsoft indica no fiarse de ArchiveStatus: puede decir None con un archivo activo. Por eso se revisan también ArchiveGuid y ArchiveDatabase.
$guid = "$($b.ArchiveGuid)"
$archivoActivo = "$($b.ArchiveStatus)" -eq 'Active' -or ($guid -and $guid -ne '00000000-0000-0000-0000-000000000000' -and "$($b.ArchiveDatabase)")
$tamano = ''
$estado = 'OK'
$nota = ''
if ($archivoActivo) {
if ($cuenta) {
try {
$tamano = ConvertTo-Gb (Get-EXOMailboxStatistics -Identity $cuenta -Archive -ErrorAction Stop).TotalItemSize
} catch {
$tamano = 'No se pudo consultar'
$estado = 'No se pudo consultar'
$nota = "$_"
}
} else {
# Sin identidad no se llama: el cmdlet interpretaría un valor vacío como todos los buzones.
$tamano = 'No se pudo consultar'
$estado = 'No se pudo consultar'
$nota = 'El buzón no trae UPN ni correo'
}
}
$bloqueos = @()
$excluidas = @()
foreach ($h in @($b.InPlaceHolds | Where-Object { $_ })) {
$v = "$h"
if ($v -like '-mbx*') { $excluidas += ($v -replace '^-mbx', '') -replace ':.*$', '' }
elseif ($v -like 'UniH*') { $bloqueos += "eDiscovery: $v" }
elseif ($v -like 'mbx*' -or $v -like 'skp*') { $bloqueos += "Política de Purview: $v" }
else { $bloqueos += "In-Place Hold: $v" }
}
if ($b.ComplianceTagHoldApplied -eq $true) { $bloqueos += 'Etiqueta de retención aplicada' }
if ($b.DelayHoldApplied -eq $true) { $bloqueos += 'Retención diferida de 30 días' }
$duracion = "$($b.LitigationHoldDuration)"
[pscustomobject]@{
Buzon = $cuenta
Nombre = "$($b.DisplayName)"
TipoBuzon = "$($b.RecipientTypeDetails)"
ArchivoActivo = if ($archivoActivo) { 'Sí' } else { 'No' }
TamanoArchivoGB = $tamano
RetencionPorLitigio = if ($b.LitigationHoldEnabled -eq $true) { 'Sí' } else { 'No' }
FechaRetencionLitigio = if ($b.LitigationHoldEnabled -eq $true) { "$($b.LitigationHoldDate)" } else { '' }
DuracionRetencion = if ($b.LitigationHoldEnabled -ne $true) { '' } elseif (-not $duracion -or $duracion -eq 'Unlimited') { 'Indefinida' } else { $duracion }
PoliticaRetencion = if ($b.RetentionPolicy) { "$($b.RetentionPolicy)" } else { 'Sin política' }
BloqueoLegal = if ($bloqueos) { $bloqueos -join '; ' } else { 'Ninguno' }
PoliticasOrganizacion = if ($errorOrg) { 'No se pudo consultar' } else { @($politicasOrg | Where-Object { $_ -notin $excluidas }).Count }
Estado = $estado
Nota = $nota
}
})
Write-Progress -Activity 'Revisando archivo y retención' -Completed
} finally {
Disconnect-ExchangeOnline -Confirm:$false
}
$fallas = @($filas | Where-Object { $_.Estado -ne 'OK' })
Write-Host "Buzones revisados: $($filas.Count). Con archivo activo: $(@($filas | Where-Object { $_.ArchivoActivo -eq 'Sí' }).Count). En retención por litigio: $(@($filas | Where-Object { $_.RetencionPorLitigio -eq 'Sí' }).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 ($errorOrg) { throw "No se pudieron consultar las políticas de retención de la organización: $errorOrg" }
if ($fallas) { throw "No se pudo consultar el archivo de $($fallas.Count) buzones: $($fallas.Buzon -join ', ')" }