<# .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 }