Automatizar el reenvío de correos específicos en Windows Server 2025 con PowerShell
Solución ligera para reenviar correos de autenticación por IMAP/SMTP sin Outlook, sin Roundcube y sin mantener una sesión iniciada.
En mi caso necesitaba automatizar una tarea muy concreta: recibir ciertos correos de autenticación y reenviarlos automáticamente a otra persona sin depender de clientes gráficos ni de una sesión abierta en el servidor.
El servidor era un Windows Server 2025 o Windows 11 o 10 que también ejecuta otros servicios, así que la solución tenía que ser estable, ligera y totalmente autónoma.
Objetivo
- Detectar correos concretos.
- Reenviarlos automáticamente.
- No tocar el resto del buzón.
Condición clave
- Sin Outlook.
- Sin Thunderbird.
- Sin sesión iniciada.
Solución
- PowerShell.
- IMAP + SMTP.
- Tarea programada.
El problema inicial
El caso concreto era reenviar automáticamente correos de autenticación de PingIdentity. El email original llegaba con este remitente y asunto:
From: PingOne <noreply@pingidentity.com>
Subject: New Authentication Request
Además, el mensaje incluía contenido HTML con el código OTP en grande. Por eso no bastaba con reenviarlo en texto plano: había que conservar el formato original.
Por qué no usar Outlook, Thunderbird o Roundcube
| Opción | Problema | Motivo para descartarla |
|---|---|---|
| Outlook | Depende de perfil y sesión | No es ideal para un servidor 24/7 |
| Thunderbird | Cliente gráfico | Puede requerir sesión abierta |
| Roundcube | Solo es interfaz web | No automatiza procesos por sí mismo |
Por qué elegí PowerShell
PowerShell encajaba perfectamente porque ya viene integrado en Windows Server, consume pocos recursos y puede ejecutarse desde el Programador de tareas aunque no haya nadie conectado al servidor.
- No requiere Docker.
- No requiere Python.
- No requiere clientes de correo abiertos.
- Permite controlar IMAP, SMTP, logs y ventanas temporales.
Librería utilizada: Mailozaurr
Para trabajar con IMAP y SMTP desde PowerShell utilicé el módulo Mailozaurr.
Set-ExecutionPolicy RemoteSigned -Scope LocalMachine
Install-Module Mailozaurr -Scope AllUsers -Force
Diseño de la automatización
Cada 5 minutos:
↓
Conectar al servidor IMAP
↓
Registrar hora de conexión válida
↓
Buscar correos dentro del rango temporal
↓
Filtrar por remitente y asunto
↓
Reenviar automáticamente
↓
Actualizar timestamp
La clave: no depender de correos leídos/no leídos
Al principio parecía lógico usar el estado leído/no leído, pero eso podía provocar problemas. Algunos accesos IMAP pueden cambiar el estado del correo y eso no era fiable.
La solución correcta fue usar ventanas temporales: el script solo procesa correos recibidos entre la última conexión IMAP correcta y la conexión actual.
Ejemplo
10:00 → conexión correcta
10:05 → procesa correos entre 10:00 y 10:05
10:10 → procesa correos entre 10:05 y 10:10
Ventajas
- No duplica mensajes.
- No depende de leído/no leído.
- Si falla la conexión, no actualiza la marca temporal.
Estructura de archivos
C:\carpeta
├── Reenviar-Autorizacion.ps1
├── ultima-conexion.txt
└── reenviar-autorizacion.log
Configuración principal
$ImapServer = "mail.tudominio.es"
$ImapPort = 993
$SmtpServer = "mail.tudominio.es"
$SmtpPort = 587
$Usuario = "usuario@dominio.com"
$Password = "PASSWORD"
$RemitenteFiltro = "remitente@dominio.com"
$AsuntoFiltro = "asunto del mensaje"
$Destinatarios = @(
"destino@dominio.com"
)
Problemas encontrados y ajustes realizados
1. Operación cancelada
El script leía todo el buzón mensaje a mensaje. Se corrigió buscando primero por fechas y procesando solo los UID candidatos.
2. Inbox NULL
En esta instalación, $Client.Inbox devolvía NULL. Se cambió por $Client.Folder tras abrir la carpeta con Mailozaurr.
3. Parámetro SSL
La versión instalada no aceptaba -SSL. La opción válida fue -UseSsl.
Ajuste IMAP utilizado
Get-IMAPMessage -Client $Client -FolderAccess ReadOnly | Out-Null
$Folder = $Client.Folder
Ajuste SMTP utilizado
Send-EmailMessage `
-From $Usuario `
-To $Destinatarios `
-Server $SmtpServer `
-Port $SmtpPort `
-Credential $Credential `
-UseSsl `
-Subject "REENVÍO: $Asunto" `
-HTML $CuerpoHtml
Texto plano vs HTML
Inicialmente el envío se hacía en texto plano:
-Text $Cuerpo
Funcionaba, pero el correo perdía el formato visual, las imágenes y la presentación del OTP.
La solución final fue reenviar en HTML usando $Mensaje.HtmlBody y enviando el mensaje con -HTML $CuerpoHtml.
$HtmlOriginal = $Mensaje.HtmlBody
if ([string]::IsNullOrWhiteSpace($HtmlOriginal)) {
$HtmlOriginal = "<pre>$($Mensaje.TextBody)</pre>"
}
$CuerpoHtml = @"
<html>
<body>
<p><strong>Se ha recibido una autorización de acceso.</strong></p>
<hr>
<p>
<strong>De:</strong> $De<br>
<strong>Asunto:</strong> $Asunto<br>
<strong>Fecha:</strong> $FechaCorreo
</p>
<hr>
$HtmlOriginal
</body>
</html>
"@
Logs y depuración
El script registra toda la actividad en un archivo de log:
C:\xtres\reenviar-autorizacion.log
Ejemplo de salida:
2026-05-07 10:11:17 - Conexión IMAP correcta
2026-05-07 10:11:18 - Correo válido encontrado
2026-05-07 10:11:19 - Correo reenviado
Configuración como tarea programada
Para que funcione de forma automática, el script se ejecuta desde el Programador de tareas de Windows.
- Crear tarea, no tarea básica.
- Marcar “Ejecutar tanto si el usuario inició sesión como si no”.
- Marcar “Ejecutar con los privilegios más altos”.
- Repetir cada 5 minutos indefinidamente.
Programa
powershell.exe
Argumentos
-ExecutionPolicy Bypass -File "C:\carpeta\Reenviar-Autorizacion.ps1"
Resultado final
- Automatización completamente autónoma.
- Sin Outlook ni clientes gráficos.
- Sin sesión iniciada.
- Funcionando 24/7.
- Consumo mínimo.
- Reenvío conservando el HTML original.
Con esta base se podría ampliar fácilmente el sistema para enviar notificaciones a Telegram, Discord, Webhooks, APIs o cualquier otro sistema externo.
Conclusión: para una automatización sencilla, estable y sin interfaz gráfica, PowerShell + IMAP + SMTP es una solución muy eficaz en Windows Server.
Seguridad y buenas prácticas
Protege las credenciales
En el ejemplo las credenciales aparecen directamente en el script para simplificar la explicación, pero en producción es recomendable usar credenciales cifradas o cuentas específicas con permisos limitados.
Usa SSL/TLS
La conexión IMAP y SMTP debe realizarse siempre mediante SSL/TLS para evitar enviar credenciales y mensajes en texto claro por la red.
Preguntas frecuentes
¿El script marca los correos como leídos?
No. El buzón se abre en modo ReadOnly, por lo que los mensajes no cambian de estado.
¿Puede funcionar aunque nadie haya iniciado sesión?
Sí. Al ejecutarse mediante el Programador de tareas de Windows, el script puede funcionar completamente en segundo plano.
¿Puede reenviar varios tipos de correos?
Sí. Basta con añadir más filtros de remitente, asunto o contenido.
¿Qué ocurre si el servidor se reinicia?
La tarea programada volverá a ejecutarse automáticamente al arrancar Windows.
Una automatización sencilla suele ser más estable y mantenible que una solución excesivamente compleja.
Import-Module Mailozaurr
# ==============================
# CONFIGURACIÓN
# ==============================
$ImapServer = "mail.dominio.es"
$ImapPort = 993
$SmtpServer = "mail.dominio.es"
$SmtpPort = 587
$Usuario = "usuario"
$Password = "clave"
$RemitenteFiltro = "dominio@dominio.com"
$AsuntoFiltro = "asunto email"
$Destinatarios = @(
"destino@dominio.com"
)
# con "persona2@dominio.com",
$CarpetaTrabajo = "C:\carpeta"
$ArchivoUltimaConexion = "$CarpetaTrabajo\ultima-conexion.txt"
$Log = "$CarpetaTrabajo\reenviar-autorizacion.log"
# ==============================
# FUNCIONES
# ==============================
function Escribir-Log {
param([string]$Texto)
if (!(Test-Path $CarpetaTrabajo)) {
New-Item -ItemType Directory -Path $CarpetaTrabajo -Force | Out-Null
}
"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Texto" |
Out-File $Log -Append -Encoding UTF8
}
# ==============================
# INICIO
# ==============================
try {
if (!(Test-Path $CarpetaTrabajo)) {
New-Item -ItemType Directory -Path $CarpetaTrabajo -Force | Out-Null
}
Escribir-Log "Iniciando conexión IMAP"
$SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force
$Credential = New-Object System.Management.Automation.PSCredential($Usuario, $SecurePassword)
$Client = Connect-IMAP `
-Server $ImapServer `
-Port $ImapPort `
-Credential $Credential `
-Options Auto
$HoraConexionActual = Get-Date
Escribir-Log "Conexión IMAP correcta: $HoraConexionActual"
if (!(Test-Path $ArchivoUltimaConexion)) {
$HoraConexionActual.ToString("o") | Set-Content $ArchivoUltimaConexion -Encoding UTF8
Escribir-Log "Primera conexión registrada. No se procesan correos antiguos."
Disconnect-IMAP -Client $Client
exit
}
$UltimaConexion = [DateTime]::Parse((Get-Content $ArchivoUltimaConexion))
Escribir-Log "Ventana de búsqueda: desde $UltimaConexion hasta $HoraConexionActual"
# Abrir carpeta INBOX usando Mailozaurr
Get-IMAPMessage -Client $Client -FolderAccess ReadOnly | Out-Null
$Folder = $Client.Folder
if ($null -eq $Folder) {
Escribir-Log "ERROR: No se ha podido abrir la carpeta INBOX."
Disconnect-IMAP -Client $Client
exit 1
}
$Reenviados = 0
# Búsqueda amplia por día para evitar problemas con horas/zonas horarias IMAP
$FechaDesdeBusqueda = $UltimaConexion.Date.AddDays(-1)
$FechaHastaBusqueda = $HoraConexionActual.Date.AddDays(1)
$Query = [MailKit.Search.SearchQuery]::DeliveredAfter($FechaDesdeBusqueda).And(
[MailKit.Search.SearchQuery]::DeliveredBefore($FechaHastaBusqueda)
)
$Uids = $Folder.Search($Query)
Escribir-Log "Correos candidatos por fecha IMAP: $($Uids.Count)"
foreach ($Uid in $Uids) {
try {
$Mensaje = $Folder.GetMessage($Uid)
$FechaCorreo = $Mensaje.Date.DateTime.ToLocalTime()
$De = $Mensaje.From.ToString()
$Asunto = $Mensaje.Subject
Escribir-Log "Revisando correo: $FechaCorreo - $De - $Asunto"
if (
$FechaCorreo -gt $UltimaConexion -and
$FechaCorreo -le $HoraConexionActual -and
$De -like "*$RemitenteFiltro*" -and
$Asunto -eq $AsuntoFiltro
) {
Escribir-Log "Correo válido encontrado: $Asunto - $FechaCorreo"
$HtmlOriginal = $Mensaje.HtmlBody
if ([string]::IsNullOrWhiteSpace($HtmlOriginal)) {
$HtmlOriginal = "<pre>$($Mensaje.TextBody)</pre>"
}
$CuerpoHtml = @"
<html>
<body>
<p><strong>Se ha recibido una autorización de acceso.</strong></p>
<hr>
<p>
<strong>De:</strong> $De<br>
<strong>Asunto:</strong> $Asunto<br>
<strong>Fecha:</strong> $FechaCorreo
</p>
<hr>
$HtmlOriginal
</body>
</html>
"@
Send-EmailMessage `
-From $Usuario `
-To $Destinatarios `
-Server $SmtpServer `
-Port $SmtpPort `
-Credential $Credential `
-UseSsl `
-Subject "REENVÍO: $Asunto" `
-HTML $CuerpoHtml
$Reenviados++
Escribir-Log "Correo reenviado a: $($Destinatarios -join ', ')"
}
}
catch {
Escribir-Log "ERROR leyendo/procesando correo UID $Uid : $($_.Exception.Message)"
continue
}
}
$HoraConexionActual.ToString("o") | Set-Content $ArchivoUltimaConexion -Encoding UTF8
Escribir-Log "Última conexión actualizada a: $HoraConexionActual"
Escribir-Log "Proceso finalizado. Correos reenviados: $Reenviados"
Disconnect-IMAP -Client $Client
}
catch {
Escribir-Log "ERROR GENERAL: $($_.Exception.Message)"
try {
if ($Client) {
Disconnect-IMAP -Client $Client
}
}
catch {}
exit 1
}
