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ónProblemaMotivo para descartarla
OutlookDepende de perfil y sesiónNo es ideal para un servidor 24/7
ThunderbirdCliente gráficoPuede requerir sesión abierta
RoundcubeSolo es interfaz webNo 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
}

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *