Sam Leiton

Desarrollador en AI, Servidores y Apps Web | Innovación y Tecnología a tu Alcance

Cómo Reenviar Correos con AWS SES sin Perder el Remitente Original: Manipulación de Encabezados y Reply-To

En este artículo compartiré en detalle el método que desarrollé para manipular correos electrónicos en procesos de reenvío. La idea central es evitar problemas y complicaciones inherentes a la configuración de Postfix, pues en mi casa tuve muchos problemas con postsrsd, nunca pude identificar el problema y mis clientes me pedían rápido una solución, así que mire lo que tenía en mano, aprovechando servicios como AWS SES, SNS y S3 para lograr una solución robusta, flexible y que se adapte a mis necesidades, aun sin tener conocimientos profundos de Postfix.

Contexto y Desafíos Iniciales

Durante mucho tiempo me enfrenté a la problemática de los reenvíos de correos electrónicos. En particular, la necesidad de reenviar mensajes sin perder la trazabilidad del remitente original generaba varios inconvenientes. El flujo típico que se presentaba era el siguiente:

y sabemos que WorkMail es una solución excelente, siempre que tengamos dinero y queramos una salida fácil sin quemarnos las pestañas durante el proceso, así que ignoremos ese y enfoquémonos en los otros 3.

Vamos por partes para entender y usar esos otros 3 servicios de aws.

1. Recepción del Correo

Un usuario externo (user@gmail.com) envía un correo a una dirección de mi dominio, por ejemplo, info@midomain.com. Esta dirección está configurada en AWS SES para recibir correos.

2. Procesamiento a través de AWS SES

AWS SES (Simple Email Service) recibe el correo y lo procesa usando registros DNS configurados para recibir correos entrantes (ses_inbound_endpoints). Y porque hacer asi? pues, SES ofrece protección a los correos entrantes por sus endpoints, además, poder manipular facil los correos para futuros diabluras que quieras hacer con ellos y los servicios de AWS. si no, simplemente ignora estos pasos y manipula el proyecto y la idea a tus necesidades.

¿Cuál es la restricción aquí?
SES solo permite enviar correos desde direcciones o dominios verificados. Es decir, no puedo reenviar directamente el correo con user@gmail.com en el campo From, ya que SES lo rechazaría.

3. Almacenamiento en S3 con Notificación Inteligente

El correo recibido se almacena en un bucket de Amazon S3. En este punto, hay un problema a resolver:

  • Opción incorrecta: Enviar una notificación directa de SES a SNS. Esto incluiría el contenido completo del correo en el mensaje de SNS, lo que puede hacer que la notificación falle si el correo es muy grande.
  • Solución correcta: Usar una función AWS Lambda como intermediaria.

¿Cómo funciona la Lambda?
En lugar de mandar el correo completo a SNS, la Lambda toma solo la información esencial (como el messageId, que es el identificador único del correo en S3) y la envía a SNS.

📌 Ejemplo de Lambda que procesa la notificación y la manda a SNS:

import json
import boto3

sns_client = boto3.client('sns')

def lambda_handler(event, context):
try:
# Extraer la notificación de SES
ses_notification = event['Records'][0]['ses']

# Enviar solo la información relevante a SNS
response = sns_client.publish(
TopicArn='arn:aws:sns:us-east-1:734680768211:MailToServerToro',
Message=json.dumps({
"messageId": ses_notification['mail']['messageId'],
"from": ses_notification['mail']['source'],
"subject": ses_notification['mail']['commonHeaders']['subject']
}),
Subject='SES Notification: New Email'
)
return {"status": "Success", "response": response}
except Exception as e:
print(f"Error: {str(e)}")
return {"status": "Error"}

¿Por qué usar Lambda?

  • Eliminamos el contenido pesado del correo en la notificación.
  • Solo enviamos los datos clave al servidor (como el messageId, que se usará para descargar el correo después).

4. Descarga del Correo y Encolado en el Servidor

Una vez que SNS recibe la notificación, la envía a mi servidor a través de HTTP o HTTPS.

En mi servidor, uso un script PHP (o cualquier otro lenguaje) para:

  1. Leer la notificación.
  2. Extraer el messageId.
  3. Descargar el correo desde S3 usando ese messageId.

Ejemplo de descarga con AWS CLI:

aws s3 cp s3://mi-bucket-ses-emails/messageId.eml /ruta/local/email.eml
  1. Ponerlo en cola con Postfix en Ubuntu:
/usr/sbin/sendmail -t < /ruta/local/email.eml

Esto reenvía el correo para su procesamiento y entrega final.

Hasta ahora todo bien, los correos recibidos son puestos en cola, al revisar la bandeja de correo de mi servidor local, podia ver claramente los correos entrantes.

pero… aquí otros puntos extra:

  1. Manipulación para reenvío: Aquí surgía el dilema. Para reenviar el correo a un destinatario final (por ejemplo, user2@gmail.com), era necesario cambiar el encabezado From para cumplir con las políticas de SES. aquí postsrsd no me funcionó, de echo tuve que rehacer el servidor de correo 2 veces porque de pronto los correos no eran enviados. No encontré el problema y tampoco tenia mucho tiempo, así que todo Forward, el From se cambio por “forward@midomain.com” Sin embargo, este cambio provocaba que, al responder, la respuesta se dirigiera a la dirección modificada (forward@midomain.com) y no al remitente original.
  2. Manipulation de encabezados: En mi caso es php, pero podemos consultar a ChatGpt como hacerlo diferente en otros lenguajes. pero asi lo hago yo
    protected function getHeader(string $header, $default = null){
    $pattern = '/^' . preg_quote($header, '/') . ': (.+)$/mi';
    $from = preg_match($pattern, $this->rawEmail, $matches) ? $matches[1] : $default;
    return $from;
    }

    protected function addHeader(string $header, string $value, $rawEmail){ return preg_replace('/^' . preg_quote($header, '/') . ': (.+)$/mi', $header . ': ' . $value, $rawEmail); }

    y si queremos cambiar un encabezado, por ejemplo el Subject:
    $rawEmail = preg_replace('/^Subject: (.+)$/mi', "Subject: $subject", $rawEmail);
  3. Conseguir los Forward correspondientes a cada correo. En mi caso uso plesk para manejar mis páginas y correos, asi que para conseguir lo correos forward

 

use GuzzleHttp\Client;

class...

protected function checkForwards($domain, $emails)
{
$forwardingAddresses = [];
$client = new Client();
$pleskUrl = $this->pleskUrl ?? 'https://'.$domain.':8443';
$url = $pleskUrl . '/api/v2/cli/mail/call'; // if u want use current domain or use default
$auth = ['pleskusername', 'pleskpassword']; // plesk user for run cli commands, basic auth user:pass    
foreach ($emails as $email) {

        $params = ["params" => ["--info", $email]];

        try {
            $response = $client->post($url, [
                'auth' => $auth,
                'json' => $params,
                'verify' => false,
            ]);

            $data = json_decode($response->getBody(), true);

            if ($data['code'] === 0 && isset($data['stdout'])) {
                preg_match('/Group member\(s\):\s+(.*)\n/', $data['stdout'], $matches);
                $forwards = isset($matches[1]) ? explode(', ', trim($matches[1])) : [];
                $forwardingAddresses = array_merge($forwardingAddresses, $forwards);
            }

        } catch (\Exception $e) {
            Log::error('Error checking forwards: ' . $e->getMessage());
        }
    }

    return $forwardingAddresses;
}

Pero si usas otros sistemas, dont worry, siempre existe una manera de hacer lo mismo, sino, la inventas.

La Solución: Encabezados para una Comunicación Efectiva

Para resolver este problema, decidí manipular los encabezados del correo reenviado. La clave fue utilizar el encabezado Reply-To, que permite especificar una dirección de respuesta diferente a la que aparece en el From. De esta manera, a pesar de que SES exige cambiar el remitente por motivos de autorización, el destinatario final tiene la posibilidad de responder directamente al correo original.

¿Cómo Funciona?

  1. Cambio de From para cumplir con SES: Al reenviar el correo, modifico el From a una dirección autorizada (forward@midomain.com). Esto es esencial para que AWS SES acepte y envíe el mensaje sin inconvenientes.
  2. Uso del encabezado Reply-To: Simultáneamente, agrego el encabezado: Reply-To: user1@gmail.com De este modo, cuando user2@gmail.com reciba el mensaje y decida responder, su cliente de correo utilizará el Reply-To y enviará la respuesta directamente a user1@gmail.com, preservando la continuidad de la comunicación.
  3. Encabezados Adicionales: Aunque el Reply-To es suficiente para redirigir las respuestas, en algunos casos también es útil incluir encabezados adicionales como Resent-From o X-Original-From para mantener un registro completo del remitente original. Estos encabezados no influyen en la respuesta del destinatario, pero ayudan en el rastreo y depuración de los correos reenviados.

 

Ventajas de este Enfoque

  • Compatibilidad con SES: Al cambiar el From, se evita el rechazo de SES por utilizar direcciones no autorizadas.
  • Preservación de la Comunicación: Gracias al Reply-To, se asegura que las respuestas se dirijan al remitente original, manteniendo la integridad del hilo de conversación.
  • Flexibilidad en el Procesamiento: La solución permite manipular otros encabezados o realizar ajustes adicionales sin depender de configuraciones complejas en Postfix.
  • Facilidad de Implementación: Al no ser un experto en Postfix, esta solución me permitió evitar la necesidad de profundizar en configuraciones avanzadas y códigos complejos.

Leave a Reply

Your email address will not be published. Required fields are marked *.

*
*