Saltar al contenido
MS Scripts

Scripts de Exchange

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.

Antes de ejecutarloBloqueoLegal muestra identificadores, no nombres: para saber qué caso o política es, búscalos en Microsoft Purview. Las políticas nuevas de Teams, Copilot y Viva Engage no se marcan en el buzón, así que este reporte no las ve. Si un archivo falla, sale como "No se pudo consultar" y el script termina con error después de guardar el CSV.

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.csv

Obtienen 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

  1. Instala el módulo si no lo tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
  2. Ejecuta el script con -RutaCsv; usa -TipoBuzon si solo te interesan los compartidos.
  3. Filtra en Excel por RetencionPorLitigio y BloqueoLegal para ver qué buzones no pueden purgarse.
  4. Revisa también PoliticasOrganizacion: esas políticas de Microsoft Purview aplican aunque BloqueoLegal diga Ninguno.
  5. 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 ', ')" }

Relacionados