Cada aplicación dispone de Correos → Historial, Plantillas, Reglas y Configuración. El canal empieza desactivado. Las credenciales SMTP pertenecen al servidor y nunca se introducen en la consola ni se entregan al navegador.
#Primera prueba
- Inicia PostgreSQL, API, worker, consola, checkout y Mailpit con Docker Compose.
- Abre una aplicación y entra en Correos → Plantillas → Crear plantillas iniciales. Se crean 18 plantillas publicadas: nueve eventos en español e inglés. Repetir la acción conserva las personalizaciones.
- Abre una plantilla. Edita asunto, preencabezado, color y bloques; comprueba HTML y texto, vista de escritorio y móvil. Guarda y publica el borrador.
- En Configuración, elige Captura local, remitente permitido y activa el canal. Comprobar conexión solicita al worker una verificación SMTP sin enviar un correo.
- En Reglas, elige evento, audiencia, plantilla publicada y destinatario. Evaluar muestra el resultado para un pago sin crear ni enviar mensajes.
- Completa un pago o pulsa Enviar prueba en el editor. Consulta el historial y abre Mailpit local para ver el mensaje capturado.
Mailpit escucha SMTP en 1025, no reenvía mensajes a Internet y no exige cuentas
reales. Desde Compose, API y worker usan mailpit; desde un proceso local se usa
localhost. Si otra instancia ocupa esos puertos, usa un archivo Compose de
sobrescritura con puertos libres y ajusta las URL de prueba.
#Qué dispara un correo
| Evento | Momento |
|---|---|
payment.requires_action |
El pago queda pendiente de la intervención del comprador |
payment.approved |
El comprador aprueba la compra, antes del cobro |
payment.authorized |
Se reserva el importe |
payment.captured |
Se confirma el cobro simulado |
payment.failed |
El pago se rechaza |
payment.canceled |
Se cancela el pago |
payment.refund_created |
Cada devolución, incluso una devolución parcial |
payment.disputed |
Se registra una disputa |
payment.settled |
Se libera el importe retenido al comercio |
Los eventos nacen en el servicio de pagos y liquidación, incluidos checkout y temporizadores. No dependen de la URL ni de la activación de webhooks. Leer un pago, actualizar sus metadatos o repetir una petición idempotente no envía otro aviso.
El destinatario comprador se obtiene del contacto del pago o de la cuenta activa
identificada por su walletKey. El checkout permite correo opcional y utiliza la
sesión también al pagar con tarjeta. El operador puede guardar un contacto antes
de finalizar el pago. Para el comercio se elige propietario o hasta diez
direcciones explícitas. No se modifican campos del contrato de Stripe, PayPal o
Mercado Pago para almacenar este contacto.
La regla selecciona una plantilla y su idioma explícitamente. No se traducen borradores ni se sustituye automáticamente una plantilla personalizada por otra. El contacto conserva la preferencia de idioma para posteriores ampliaciones de selección de traducciones; actualmente el idioma efectivo es el de la plantilla.
#Persistencia y estados
El pago y la obligación de correo se escriben en una misma transacción PostgreSQL.
La obligación fija operación, destinatario, versión, documento, datos, remitente
y perfil. El worker genera HTML y texto con el mismo renderizador MJML que usa
la vista previa y realiza SMTP después del commit. Los importes se formatean
desde unidades menores bigint, incluyendo el importe de cada devolución.
| Estado | Significado y acción |
|---|---|
pending |
Pendiente; se puede cancelar |
processing |
Reclamado por un worker |
retry_scheduled |
Fallo temporal conocido; próximo intento programado |
captured |
Aceptado por el SMTP local de captura |
accepted |
Aceptado por SMTP externo; no demuestra llegada al buzón |
failed |
Fallo definitivo o intentos agotados; admite reintento manual |
unknown |
Pudo transmitirse antes de perder la confirmación; requiere revisión |
skipped |
Sin destinatario, plantilla o transporte utilizable; conserva motivo |
canceled |
Cancelado por el operador o por aplicación inactiva |
Un fallo temporal permite tres reintentos: 30 segundos, 2 minutos y 10 minutos.
Los reclamos tienen un lease de 60 segundos y un token de comprobación; SMTP tiene
un límite global de 20 segundos. Dos workers usan bloqueos con SKIP LOCKED.
Tras perder un worker, un reclamo sin transmisión vuelve a pendiente; uno que
inició transmisión pasa a unknown. SMTP no ofrece garantía de exactamente una
entrega; esos resultados nunca se repiten automáticamente.
Reintentar conserva el mensaje, la operación original y todos sus intentos. Reenviar crea otro mensaje enlazado al original. Ambas acciones requieren motivo, se registran con actor y fecha y tienen una clave de petición idempotente. El límite predeterminado es 20 acciones de envío manual por hora, por aplicación y por actor. Las comprobaciones de conexión se limitan a tres por minuto.
Desactivar el canal impide obligaciones nuevas. Pausarlo detiene las existentes. Cambiar de perfil no redirige mensajes pendientes: conservan el perfil fijado; si cambió su host, puerto o TLS, quedan omitidos con un motivo visible. Los mensajes terminales conservan el contenido 30 días por defecto; después se eliminan destinatario, documento, asunto, HTML, texto y contexto del registro.
#Configuración del servidor
API y worker deben recibir la misma configuración. El perfil real exige habilitación explícita y TLS con certificado verificado.
| Variable | Valor predeterminado / uso |
|---|---|
EMAIL_CAPTURE_HOST, EMAIL_CAPTURE_PORT |
localhost, 1025; Compose fija host mailpit |
EMAIL_TRANSPORT |
disabled bloquea todo transporte |
EMAIL_EXTERNAL_ENABLED |
Solo true habilita el perfil SMTP real |
EMAIL_SMTP_HOST, EMAIL_SMTP_PORT |
Servidor autorizado, puerto 587 |
EMAIL_SMTP_TLS_MODE |
starttls o tls (normalmente puerto 465) |
EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD |
Credenciales del servidor, opcionales según proveedor |
EMAIL_ALLOWED_FROM |
Lista separada por comas; sandbox@laboratorio.test |
EMAIL_LINK_ORIGINS |
Orígenes permitidos para botones; por defecto URL pública del checkout |
EMAIL_MANUAL_HOURLY_LIMIT |
20 |
EMAIL_RETENTION_DAYS |
30 |
Las plantillas son documentos de bloques, no HTML arbitrario. Solo admiten las variables del catálogo y enlaces HTTP(S) de los orígenes permitidos. La preview usa un iframe aislado sin scripts, formularios, navegación privilegiada ni recursos remotos. No hay archivos adjuntos, imágenes externas ni inclusión MJML.
#API y código
La raíz es /applications/:applicationId/emails. Incluye catalog, settings,
templates, versiones y publicación, rules y evaluación, test-messages,
messages, intentos, acciones y connection-checks. Listas con el contrato
común {data, meta}; mutaciones de configuración con revision y conflicto 409.
El contacto se guarda con PATCH /applications/:applicationId/payments/:intentId/contact.
Todos los recursos se limitan a administradores y al comercio propietario;
un identificador de otra aplicación responde 404.
#Ejecución local y despliegue
En Windows, run-local.bat carga las variables de correo de .env y .env.local
para API y worker. Inicia Mailpit por separado y configura EMAIL_CAPTURE_PORT
si no utiliza 1025. El pago puede ejecutarse sin un servidor SMTP disponible;
el historial conserva los fallos del transporte.
El despliegue nativo inicializa las variables en /etc/<proyecto>/api.env, que
también lee el worker. Conserva los valores editados al actualizar. El Compose
de producción comparte las mismas variables entre ambos procesos. Ambos
despliegues comienzan con EMAIL_TRANSPORT=disabled; para habilitar un perfil,
cambia ese valor a capture o smtp y configura su servidor. SMTP externo exige
además EMAIL_EXTERNAL_ENABLED=true. Reinicia API y worker tras cambiar el entorno.
El despliegue de producción no instala Mailpit automáticamente.
npm run smoke:emails comprueba pago, worker, Mailpit y mensaje MIME.
npm run smoke:emails:browser comprueba consola ES/EN, temas claro/oscuro y
anchos de 1440/390 px. Ambos usan el usuario de pruebas de smoke:stack.
Admiten API_URL, CONSOLE_URL, MAILPIT_URL, SMOKE_EMAIL y SMOKE_PASSWORD.
Si defines MAILPIT_URL, npm run smoke:stack incluye la prueba de correos.
Las capturas, el informe de accesibilidad y captured.eml quedan en
.tmp/shots/emails/. El MIME procede del buzón; no es una reconstrucción del HTML.
La auditoría automática cubre la consola y el foco del diálogo; excluye el iframe
aislado del correo. La compatibilidad con clientes de correo externos requiere
una comprobación independiente y no se deduce de las capturas del navegador.
El módulo está en apps/api/src/modules/emails; la interfaz, en
apps/console/app/modules/integrations/emails. Las migraciones crean plantillas,
versiones, reglas, configuración, mensajes, intentos, acciones y diagnósticos.
Pruebas reproducibles: apps/api/test/email-renderer.spec.ts,
apps/api/test/integration/emails.spec.ts,
apps/api/test/integration/emails-lifecycle.spec.ts y scripts/emails-browser.mjs.
Referencias de los motores utilizados: SMTP de Nodemailer, MJML y Mailpit.
#Plantillas HTML importadas y sanitización
Las plantillas con type: "html" se consideran contenido no confiable. La API normaliza y sanitiza el HTML en el servidor antes de almacenarlo como borrador, vuelve a aplicar la misma frontera antes de publicar una versión y sanitiza otra vez después de interpolar variables antes de generar el mensaje final. El HTML original inseguro nunca se convierte en el documento canónico autorizado.
La política H1 permite estructura típica de correo, tablas, texto, enlaces e inline CSS de presentación mediante listas explícitas. Rechaza contenido activo (script, iframe, formularios, SVG/MathML y elementos equivalentes), atributos on*, recursos src/srcset/poster/background, CSS que pueda cargar o ejecutar contenido (url(), @import, expression(), esquemas javascript:, data:, file: y equivalentes) y documentos mayores de 200 KB. Las imágenes y otros recursos remotos no forman parte de H1.
Los enlaces sólo pueden usar HTTP/HTTPS y deben pertenecer a un origen autorizado por EMAIL_LINK_ORIGINS; si no está definido, se usa CHECKOUT_PUBLIC_URL y finalmente http://localhost:3001 como valor local de desarrollo. No se permiten credenciales embebidas ni URLs protocol-relative. Los enlaces aceptados se normalizan con rel="noopener noreferrer".
Las variables continúan limitadas al catálogo oficial de Correos. Sus valores se escapan antes de insertarse en HTML y el resultado interpolado se vuelve a sanitizar, por lo que datos como <script>...</script> se muestran como texto y no se ejecutan. Si una plantilla HTML no define text, el backend deriva una alternativa de texto plano desde el HTML ya sanitizado.
La certificación cubre tanto la unidad de sanitización como el contrato HTTP/BD: crear, previsualizar y publicar una plantilla HTML segura persiste únicamente la representación sanitizada, mientras que scripts, tracking remoto, handlers, CSS inseguro y enlaces externos reciben 400 antes de convertirse en un borrador autorizado.