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