Saltar al contenido
MS Scripts

Scripts de Exchange

Rastreo de mensajes

Responde "¿llegó ese correo?" buscando por remitente, destinatario, fechas y asunto, con estado de entrega y hora en México. Avisa si el resultado quedó cortado por el tope.

Solo lecturaSin permisos de GraphProbado con respuestas simuladas. Sin ejecutar en un tenant real.

Antes de ejecutarloEl resultado trae datos personales de terceros: correos y asuntos. Compártelo solo con quien lo necesite y no lo guardes más de lo necesario. Get-MessageTraceV2 reemplaza a Get-MessageTrace; llega hasta 90 días atrás, acepta 10 días por búsqueda y máximo 5000 resultados, y no pagina: el script no pide los siguientes, avisa cuando se alcanza el tope. Microsoft limita a 100 consultas cada 5 minutos.

Ejemplo en México

Despacho contable en Guadalajara, 35 personas

En pleno cierre fiscal, un cliente asegura que mandó su balanza de comprobación el lunes y la contadora no la ve en su bandeja.

.\search-rastreo-mensajes.ps1 -Remitente cliente@empresa.example -Destinatario contadora@empresa.example -Desde "2026-11-02" -Hasta "2026-11-05" -RutaCsv .\rastreo.csv

El mensaje aparece como Quarantined y con su hora de México. Lo liberan de la cuarentena y agregan una nota para la próxima revisión de la política antispam.

Cómo implementarlo

  1. Instala o actualiza el módulo: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
  2. Ejecuta el script con al menos un filtro y un rango de hasta 10 días; sin fechas busca las últimas 48 horas.
  3. Revisa la columna Estado: Delivered, Failed, Quarantined o FilteredAsSpam, entre otros.
  4. Si TopeAlcanzado dice Sí, reduce el rango o agrega filtros: faltan mensajes.
  5. Comparte con el área afectada solo lo necesario y borra el CSV cuando termines.
Ver el código completo de search-rastreo-mensajes.ps1
<#
.SYNOPSIS
    Rastrea mensajes por remitente, destinatario, fechas y asunto para saber si un correo llegó y qué pasó con él.

.DESCRIPTION
    Usa Get-MessageTraceV2, que reemplaza a Get-MessageTrace. Consulta hasta 90 días atrás, con un rango máximo de 10 días
    por búsqueda y un tope de 5000 resultados. Si se alcanza el tope, avisa: reduce el rango o agrega filtros.
    Sin fechas busca las últimas 48 horas. Las fechas que escribes se toman en la hora de tu computadora.
    Solo lectura: no cambia nada en el tenant. El resultado trae datos personales: correos y asuntos.

.PARAMETER Remitente
    Uno o más correos de remitente.

.PARAMETER Destinatario
    Uno o más correos de destinatario.

.PARAMETER Desde
    Inicio del rango, por ejemplo "2026-11-01 08:00". Si se omite, 48 horas antes de -Hasta.

.PARAMETER Hasta
    Fin del rango. Si se omite, ahora. Una fecha sin hora incluye ese día completo.

.PARAMETER Asunto
    Texto que debe contener el asunto.

.PARAMETER Maximo
    Número máximo de resultados, de 1 a 5000. Por defecto, 5000.

.PARAMETER ZonaHoraria
    Zona horaria para mostrar la hora de recepción. Por defecto, Central Standard Time (Mexico).

.PARAMETER RutaCsv
    Ruta del CSV de salida. Si se omite, solo se muestra en pantalla.

.EXAMPLE
    .\search-rastreo-mensajes.ps1 -Remitente proveedor@empresa.example -Destinatario ana@empresa.example -Desde "2026-11-01" -Hasta "2026-11-05" -RutaCsv .\rastreo.csv

.EXAMPLE
    .\search-rastreo-mensajes.ps1 -Asunto "Factura" -RutaCsv .\rastreo.csv

.NOTES
    Permisos: ninguno de Graph; usa ExchangeOnlineManagement.
    Rol mínimo: View-Only Recipients en Exchange Online (incluido en Compliance Management, Help Desk, Hygiene Management, View-Only Organization Management y Organization Management).
    Licencia: Exchange Online.
    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(
    [string[]]$Remitente,
    [string[]]$Destinatario,
    [datetime]$Desde,
    [datetime]$Hasta,
    [string]$Asunto,
    [ValidateRange(1, 5000)][int]$Maximo = 5000,
    [string]$ZonaHoraria = 'Central Standard Time (Mexico)',
    [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 }

try { $zona = [TimeZoneInfo]::FindSystemTimeZoneById($ZonaHoraria) } catch { throw "Zona horaria no reconocida: $ZonaHoraria" }

$ahora = [datetime]::UtcNow
# "2026-11-05" significa medianoche: sin este ajuste se perdería todo ese día.
$fin = if ($PSBoundParameters.ContainsKey('Hasta')) { $(if ($Hasta.TimeOfDay -eq [timespan]::Zero) { $Hasta.AddDays(1) } else { $Hasta }).ToUniversalTime() } else { $ahora }
if ($fin -gt $ahora) { $fin = $ahora }
$inicio = if ($PSBoundParameters.ContainsKey('Desde')) { $Desde.ToUniversalTime() } else { $fin.AddHours(-48) }
if ($inicio -gt $ahora) { throw 'Rango inválido: -Desde está en el futuro.' }
if ($inicio -ge $fin) { throw 'Rango inválido: -Desde debe ser anterior a -Hasta.' }
if (($fin - $inicio).TotalDays -gt 10) { throw 'Rango inválido: Get-MessageTraceV2 acepta máximo 10 días por búsqueda. Parte la búsqueda en tramos.' }
if ($inicio -lt $ahora.AddDays(-90)) { throw 'Rango inválido: Get-MessageTraceV2 solo tiene datos de los últimos 90 días.' }

$filtros = @{ StartDate = $inicio; EndDate = $fin; ResultSize = $Maximo }
if ($Remitente) { $filtros.SenderAddress = $Remitente }
if ($Destinatario) { $filtros.RecipientAddress = $Destinatario }
if ($Asunto) { $filtros.Subject = $Asunto; $filtros.SubjectFilterType = 'Contains' }

Connect-ExchangeOnline -ShowBanner:$false
try {
    if (-not (Get-Command Get-MessageTraceV2 -ErrorAction SilentlyContinue)) {
        throw 'Tu módulo no trae Get-MessageTraceV2. Actualízalo con: Update-Module ExchangeOnlineManagement'
    }
    $mensajes = @(Get-MessageTraceV2 @filtros -ErrorAction Stop)
} finally {
    Disconnect-ExchangeOnline -Confirm:$false
}

$topeAlcanzado = $mensajes.Count -ge $Maximo
$filas = @(foreach ($m in $mensajes) {
    # La salida del cmdlet viene en UTC.
    $recibido = "$($m.Received)"
    if ($m.Received) {
        try {
            $fecha = [datetime]$m.Received
            if ($fecha.Kind -eq 'Unspecified') { $fecha = [datetime]::SpecifyKind($fecha, 'Utc') }
            $recibido = [TimeZoneInfo]::ConvertTime($fecha.ToUniversalTime(), $zona).ToString('yyyy-MM-dd HH:mm')
        } catch { Write-Verbose "Fecha no reconocida, se deja como llegó: $recibido" }
    }
    [pscustomobject]@{
        Recibido      = $recibido
        Remitente     = "$($m.SenderAddress)"
        Destinatario  = "$($m.RecipientAddress)"
        Asunto        = "$($m.Subject)"
        Estado        = "$($m.Status)"
        TamanoBytes   = $m.Size
        IpOrigen      = "$($m.FromIP)"
        IpDestino     = "$($m.ToIP)"
        IdMensaje     = "$($m.MessageId)"
        IdRastreo     = "$($m.MessageTraceId)"
        TopeAlcanzado = if ($topeAlcanzado) { 'Sí' } else { 'No' }
    }
})

Write-Host "Mensajes encontrados: $($filas.Count)"
if ($topeAlcanzado) { Write-Warning "Se alcanzó el tope de $Maximo resultados: puede haber más mensajes que no aparecen. Reduce el rango de fechas o agrega filtros." }
if ($RutaCsv) {
    # Excel ejecutaría como fórmula un asunto que empiece con estos caracteres, y el asunto lo elige quien envía el correo.
    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
}

Parte del playbook

Relacionados