Microservicio para enviar correos vía API usando SMTP. Incluye autenticación por token (header x-api-token) y sistema completo de plantillas.
flowchart LR
Client[Cliente/Integración]
API[ms-smtp API]
SMTP[Servidor SMTP]
FS[(Filesystem)]
PG[(Postgres)]
Client -->|HTTP + x-api-token| API
API --> SMTP
API -->|DB_PROVIDER=filesystem| FS
API -->|DB_PROVIDER=postgres| PG
classDef db fill:#e7f5ff,stroke:#6aa1ff,stroke-width:1px;
class PG db;
sequenceDiagram
participant C as Cliente
participant A as API ms-smtp
participant T as Motor de Plantillas
participant S as Servidor SMTP
C->>A: POST /api/v1/send-template (templateId, params, to...)
A->>T: renderTemplate(templateId, params)
T-->>A: subject, html, defaults
A->>S: SMTP send (Nodemailer)
S-->>A: 250 Ok / error
A-->>C: 202 Accepted (queued) o 502 Error
A->>A: logEmailEvent(status, meta)
- Node.js 18+
- Servidor SMTP accesible (host, puerto, credenciales si aplica)
- Copia
.env.examplea.envy completa los valores:cp .env.example .env
- Instala dependencias:
npm install
PORT: Puerto del servidor Express (default 3000)API_TOKEN: Token para consumir la API (requerido)SMTP_HOST: Host SMTP (requerido)SMTP_PORT: Puerto SMTP (587 típico, 465 para TLS implícito)SMTP_SECURE:truesi usas 465 (TLS implícito);falsepara STARTTLS en 587SMTP_USER,SMTP_PASS: Credenciales SMTP si son necesariasSMTP_FROM_DEFAULT: Remitente por defecto si no se envíafromen el payload
Opciones avanzadas en .env.example.
| Variable | Requerido | Default | Descripción |
|---|---|---|---|
| PORT | No | 3000 | Puerto base. El servicio hará fallback al siguiente libre. |
| API_TOKEN | Sí | - | Token para autenticar requests (header x-api-token). |
| SMTP_HOST | Sí | - | Host del servidor SMTP. |
| SMTP_PORT | Sí | 587 | Puerto SMTP. 465 para TLS implícito. |
| SMTP_SECURE | No | false | true para TLS implícito (465). |
| SMTP_USER | No | - | Usuario SMTP (si aplica). |
| SMTP_PASS | No | - | Password SMTP (si aplica). |
| SMTP_FROM_DEFAULT | No | "Nombre no-reply@example.com" | Remitente por defecto. |
| LOG_DIR | No | ./data/logs | Directorio para logs JSONL (filesystem). |
| LOG_FILE_NAME | No | email.log | Nombre de archivo de logs. |
| TEMPLATES_DIR | No | ./data/templates | Directorio de plantillas (filesystem). |
| DB_PROVIDER | No | filesystem | Cambia a postgres para usar Postgres. |
| PG_CONNECTION_STRING | No | - | Cadena de conexión completa a Postgres. |
| PG_HOST | No | localhost | Host Postgres (si no usas connection string). |
| PG_PORT | No | 5432 | Puerto Postgres. |
| PG_USER | No | postgres | Usuario Postgres. |
| PG_PASSWORD | No | - | Password Postgres. |
| PG_DATABASE | No | ms_smtp | Base de datos. |
| PG_SSL | No | false | Si la conexión usa SSL. |
| SWAGGER_PATH | No | /docs | Ruta donde se monta Swagger UI. |
Ejemplo para levantar Postgres y el microservicio:
version: '3.9'
services:
db:
image: postgres:16
environment:
POSTGRES_DB: ms_smtp
POSTGRES_USER: ms
POSTGRES_PASSWORD: ms
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
ms_smtp:
image: node:20-alpine
working_dir: /app
command: sh -c "npm ci && npm run start"
volumes:
- ./:/app
environment:
PORT: 3000
API_TOKEN: your-secure-token
SMTP_HOST: smtp
SMTP_PORT: 587
SMTP_SECURE: "false"
SMTP_USER: usuario
SMTP_PASS: clave
SMTP_FROM_DEFAULT: "Servicio <no-reply@example.com>"
DB_PROVIDER: postgres
PG_HOST: db
PG_PORT: 5432
PG_USER: ms
PG_PASSWORD: ms
PG_DATABASE: ms_smtp
PG_SSL: "false"
ports:
- "3000:3000"
depends_on:
- db
volumes:
pgdata:Si expones el servicio detrás de un reverse proxy (Nginx/Traefik), asegúrate de reenviar correctamente /api/, /health y especialmente /docs/ para que Swagger UI sirva sus assets con el MIME correcto.
- Archivo de ejemplo:
deploy/nginx.conf.example. - Puntos clave:
- Usa
location ^~ /docs/ { proxy_pass ... }para que ninguna regla de SPA/try_filescapture/docs/*y devuelva HTML. - Proxy también para
/api/y/health. - Verifica cabeceras con:
Debe responder con
curl -I https://tu-dominio/docs/swagger-ui.css
Content-Type: text/cssy estado 200. - Ajusta
server_name, upstream/puerto y, si aplica, TLS.
- Usa
Variables relevantes:
SWAGGER_PATH(default/docs): si lo cambias, actualiza tu proxy.PORT: puerto interno donde corre la app (mapea en Docker o proxy).
Con Docker en VPS típico:
docker compose up -d
# Nginx fuera del compose o en otro stack, apuntando a la app (p. ej., 127.0.0.1:3000)- Desarrollo (con nodemon):
npm run dev
- Producción:
npm start
- La app escuchará en
http://localhost:3000(o el puerto configurado). - La app se iniciará en el primer puerto disponible comenzando en
PORT(por defecto 3000). Revisa la consola para ver el puerto final.
- La app escuchará en
- Ejemplos de uso: Ver
examples/api-examples.md - Endpoints disponibles: Ver sección "Endpoint" más abajo
- Autenticación: Usar header
x-api-tokencon el valor deAPI_TOKEN
POST /api/v1/send-email
Headers:
Content-Type: application/jsonx-api-token: <tu_token>
Body (JSON):
{
"from": "Nombre <no-reply@example.com>",
"to": ["destino@example.com"],
"cc": "copia@example.com",
"bcc": ["oculto1@example.com", "oculto2@example.com"],
"subject": "Asunto de prueba",
"html": "<h1>Hola</h1><p>Este es un correo de prueba</p>",
"text": "Hola - Este es un correo de prueba",
"replyTo": "responder-a@example.com",
"attachments": [
{
"filename": "ejemplo.txt",
"content": "SG9sYQ==",
"encoding": "base64"
}
]
}- Requeridos:
to,html. - Opcionales:
from,cc,bcc,subject,text,replyTo,attachments. - Remitente efectivo: si no envías
from, configuraSMTP_FROM_DEFAULTen el entorno. - SMTP: debe existir
SMTP_HOST(y credenciales si tu servidor lo requiere).
Respuesta 202:
{
"status": "queued",
"result": {
"messageId": "<...>",
"accepted": ["destino@example.com"],
"rejected": [],
"response": "250 2.0.0 Ok: queued"
}
}curl -X POST http://localhost:3000/api/v1/send-email \
-H 'Content-Type: application/json' \
-H 'x-api-token: TU_TOKEN' \
-d '{
"to": "destino@example.com",
"subject": "Hola",
"html": "<b>Prueba</b>"
}'-
GET
/api/v1/logs(conx-api-token):- Query params opcionales:
status,to,from,contains,start,end,limit,offset. - Ejemplo:
curl 'http://localhost:3001/api/v1/logs?status=success,failed&limit=50' \ -H 'x-api-token: TU_TOKEN'
- Respuesta: objeto con
total,offset,limit,items(cada item es un evento constatus,timestamp,to,from,subject, etc.)
- Query params opcionales:
-
POST
/api/v1/logs(conx-api-token):- Permite registrar manualmente eventos como
canceledospamcuando provienen de otro sistema/feedback. - Body:
{ "status": "canceled", "to": "user@example.com", "subject": "Campaña X", "meta": { "reason": "usuario solicitó cancelación" } }
- Permite registrar manualmente eventos como
- El servicio registra automáticamente
successyfaileden el endpoint de envío. - Estados como
canceledospamusualmente provienen de feedback externo (proveedor, webhook, sistema anti-spam). Puedes registrarlos víaPOST /api/v1/logs. - Los logs se almacenan en formato JSONL en
LOG_DIR/LOG_FILE_NAME(ver.env.example).
- GET
/api/v1/smtp-check(conx-api-token): verifica conectividad/credenciales contransporter.verify().Respuesta 200:curl http://localhost:3001/api/v1/smtp-check \ -H 'x-api-token: TU_TOKEN'Si falla, devuelve 502 con{ "ok": true, "verified": true, "host": "smtp.example.com", "port": 587, "secure": false }{ ok: false, error }.
GET /health->{ status: 'ok', uptime: <segundos> }
- Mantén
API_TOKENen secreto y cámbialo regularmente. - En producción, usa
SMTP_TLS_REJECT_UNAUTH=true. - Configura correctamente CORS si expones el servicio a terceros.
Para mejorar la entregabilidad y reducir la probabilidad de spam:
- SPF: Publica un registro SPF que incluya a tu proveedor SMTP.
- Ejemplo:
v=spf1 include:sendgrid.net include:mailgun.org -all.
- Ejemplo:
- DKIM: Habilita DKIM en tu proveedor y publica las claves en DNS. Alinea el dominio del
Fromcon la firma DKIM. - DMARC: Configura DMARC para alinear SPF/DKIM.
- Inicio:
v=DMARC1; p=none; rua=mailto:dmarc@tu-dominio.com; fo=1. - Escala a
quarantine/rejectcuando esté estable.
- Inicio:
- Remitente consistente: Usa
fromdel mismo dominio que has verificado con SPF/DKIM/DMARC. En el entorno, ajustaSMTP_FROM_DEFAULT="Nombre <no-reply@tu-dominio.com>". - TLS: Usa STARTTLS/TLS válido. Si auto-hospedas, certifica el hostname.
- List-Unsubscribe: Añade encabezados para bajas con un clic si haces envíos recurrentes.
List-Unsubscribe: <mailto:unsubscribe@tu-dominio.com>, <https://tu-dominio.com/unsubscribe?u=...>List-Unsubscribe-Post: List-Unsubscribe=One-Click
- Contenido: Incluye versión
textademás dehtml, evita solo-imagen, cuida palabras típicas de spam y exceso de mayúsculas. - Enlaces: Evita acortadores, usa HTTPS y dominios de confianza.
- Adjuntos: Evita ejecutables y limita tamaño.
- Listas limpias: Doble opt‑in, depura rebotes e inactivos, calienta IP/domino gradualmente.
- Monitoreo: Google Postmaster Tools, Microsoft SNDS, métricas de rebote/queja.
Pasos prácticos inmediatos:
- Define
SMTP_FROM_DEFAULTcon tu dominio verificado. - Verifica SPF/DKIM/DMARC en tu DNS.
- Añade
textademás dehtmlcuando sea posible. - Revisa que los enlaces del HTML usen tu dominio o uno con buena reputación.
- Envío de correos vía SMTP con autenticación por token (
x-api-token). - Documentación interactiva con Swagger en
/docs. - Registro de eventos de envío (success/failed/otros) y consulta con filtros.
- Sistema de plantillas con Handlebars: CRUD + envío por plantilla.
- Fallback automático de puerto: inicia en
PORTy prueba puertos siguientes si está ocupado. - Backend de almacenamiento opcional:
- Filesystem (por defecto): logs en JSONL, plantillas en JSON.
- Postgres (opcional): tablas
email_logsyemail_templatescon migración automática.
Endpoints principales (todas requieren x-api-token):
- GET
/api/v1/templates— lista plantillas. - GET
/api/v1/templates/{id}— obtiene una plantilla. - POST
/api/v1/templates— crea una plantilla. Campos mínimos:name,subject,html. Opcionalid,defaults. - PUT
/api/v1/templates/{id}— actualiza. - DELETE
/api/v1/templates/{id}— elimina. - POST
/api/v1/send-template— envía correo desde una plantilla.
Ejemplos rápidos:
curl -X POST http://localhost:<PUERTO>/api/v1/templates \
-H 'Content-Type: application/json' -H 'x-api-token: TU_TOKEN' \
-d '{
"id": "bienvenida",
"name": "Email de Bienvenida",
"subject": "Hola {{firstName}} 👋",
"html": "<h1>Hola {{firstName}}</h1><p>Bienvenido a {{company}}</p>",
"defaults": { "from": "Soporte <no-reply@tu-dominio.com>" }
}'
curl -X POST http://localhost:<PUERTO>/api/v1/send-template \
-H 'Content-Type: application/json' -H 'x-api-token: TU_TOKEN' \
-d '{
"templateId": "bienvenida",
"params": { "firstName": "Ana", "company": "MiApp" },
"to": "ana@example.com"
}'- Activa Postgres poniendo
DB_PROVIDER=postgresen.env. - Conexión por
PG_CONNECTION_STRINGo variablesPG_HOST,PG_PORT,PG_USER,PG_PASSWORD,PG_DATABASE,PG_SSL. - En el arranque, el servicio creará automáticamente:
email_logs: eventos de envío.email_templates: plantillas.
- Si la conexión falla, se registrará en consola y se hará fallback automático a filesystem.
PORT: puerto base (por defecto 3000). Arranque usa puerto disponible.API_TOKEN: requerido para consumir la API.- SMTP:
SMTP_HOST,SMTP_PORT,SMTP_SECURE,SMTP_USER,SMTP_PASS,SMTP_FROM_DEFAULT. - Logs (filesystem):
LOG_DIR,LOG_FILE_NAME. - Plantillas (filesystem):
TEMPLATES_DIR. - Base de datos:
DB_PROVIDER=filesystem|postgres,PG_*.
- Puerto en uso: el servicio probará el siguiente automáticamente. Ver consola para el puerto final.
- Error SMTP (502): validar host/puerto,
SMTP_SECURE, credenciales y que el servidor permita relay. - Swagger sin autorización: incluir
x-api-tokenen Authorize o en cada request. - Postgres no disponible: revisar conexión/credenciales; el servicio caerá a filesystem y seguirá operativo.
Este proyecto se distribuye bajo la licencia incluida en LICENSE.
Proyecto público de eCortes.cl
- Sitio: https://eCortes.cl
- Mantención y contribuciones: PRs y issues son bienvenidos.