<# .SYNOPSIS Responde qué buzones reenvían correo y cuáles lo mandan fuera de tu organización. .DESCRIPTION Revisa ForwardingAddress, ForwardingSmtpAddress y DeliverToMailboxAndForward de cada buzón y marca como externos los destinos fuera de tus dominios aceptados, y como "Revisar destino" los que no son un buzón. Un reenvío externo es señal de cuenta comprometida o de fuga de datos. Si un destino no se puede resolver, 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 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-reenvios-de-correo.ps1 -RutaCsv .\reenvios.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( [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' } 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 } function Test-Externo($correo, $dominios) { $dominio = ("$correo" -split '@')[-1] -not ($dominios | Where-Object { $dominio -like $_ }) } # 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() Connect-ExchangeOnline -ShowBanner:$false try { $dominios = @(Get-AcceptedDomain -ErrorAction Stop | ForEach-Object { "$($_.DomainName)" }) # Sin dominios aceptados todo destino parecería externo: mejor detenerse que dar una falsa alarma. if (-not $dominios) { throw 'No se pudieron leer los dominios aceptados del tenant.' } $buzones = Get-EXOMailbox -RecipientTypeDetails $tipos -ResultSize Unlimited -Properties ForwardingAddress, ForwardingSmtpAddress, DeliverToMailboxAndForward -ErrorAction Stop | Where-Object { $_.ForwardingAddress -or $_.ForwardingSmtpAddress } foreach ($b in @($buzones)) { $smtp = "$($b.ForwardingSmtpAddress)" -replace '^smtp:' $tipoDestino = '' $destino = $smtp $alcance = $null $alertas = @() try { if ($b.ForwardingAddress) { # ForwardingAddress manda sobre ForwardingSmtpAddress, y puede apuntar a un contacto con correo externo. $destinatario = @(Get-EXORecipient -Identity "$($b.ForwardingAddress)" -Properties PrimarySmtpAddress, RecipientTypeDetails -ErrorAction Stop) if ($destinatario.Count -ne 1) { throw "El destino $($b.ForwardingAddress) no corresponde a un solo destinatario" } $destino = "$($destinatario[0].PrimarySmtpAddress)" if (-not $destino) { throw "No se pudo resolver el destinatario $($b.ForwardingAddress)" } $tipoDestino = "$($destinatario[0].RecipientTypeDetails)" if ($smtp -and (Test-Externo $smtp $dominios)) { $alertas += "ForwardingSmtpAddress externo sin efecto hoy ($smtp): investiga quién lo puso" } } $alcance = if (Test-Externo $destino $dominios) { 'Externo' } # Un usuario de correo o una lista con dirección interna pueden entregar fuera de la organización. elseif ($b.ForwardingAddress -and $tipoDestino -notmatch 'Mailbox$') { 'Revisar destino' } else { 'Interno' } if ($alcance -eq 'Revisar destino') { $alertas += "El destino no es un buzón ($tipoDestino) y puede entregar fuera de la organización" } } catch { $fallas.Add("$($b.PrimarySmtpAddress)") $alcance = "No se pudo consultar: $_" } if ($alcance -eq 'Externo') { $alertas += 'Reenvío externo: revisa si la cuenta está comprometida o si hay fuga de datos' } if ($b.DeliverToMailboxAndForward -ne $true) { $alertas += 'No deja copia en el buzón' } $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName Correo = $b.PrimarySmtpAddress Tipo = $b.RecipientTypeDetails DestinoEfectivo = $destino Alcance = $alcance GuardaCopia = if ($b.DeliverToMailboxAndForward -eq $true) { 'Sí' } else { 'No' } ForwardingAddress = "$($b.ForwardingAddress)" ForwardingSmtpAddress = "$($b.ForwardingSmtpAddress)" Alertas = $alertas -join '; ' }) } } finally { Disconnect-ExchangeOnline -Confirm:$false } $salida = @($filas | Sort-Object Alcance, Correo) Write-Host "Buzones con reenvío: $($salida.Count). Externos: $(@($salida | Where-Object Alcance -EQ 'Externo').Count)." 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 resolver $($fallas.Count) reenvíos: $($fallas -join ', '). Revisa las filas marcadas en el reporte." }