Posthorn: La Capa de Correo Saliente Unificada para Proyectos Autogestionados
Posthorn es una puerta de enlace de correo electrónico autogestionada que unifica el correo saliente de tus aplicaciones hacia proveedores transaccionales como Postmark, Resend y AWS SES.
El Problema: El Correo Saliente es un Desastre para los Autogestionados
Nadie quiere ejecutar un servidor de correo en 2026. Los operadores autogestionados usan Postmark, Resend, Mailgun o AWS SES porque son baratos, manejan la entregabilidad correctamente y alguien más se preocupa por SPF/DKIM/DMARC/rechazos/reputación del remitente.
Pero cada aplicación que autogestionas tiene que integrarse con ese servicio de forma independiente. Tu formulario de contacto. Los correos administrativos de tu blog Ghost. Tus enlaces mágicos de Gitea. Tus notificaciones de Mastodon. El Worker de Cloudflare que envía un correo de restablecimiento de contraseña cuando alguien hace clic en el enlace. Cada uno necesita su propia copia de la clave API, su propio código de integración, sus propias peculiaridades en el manejo de reintentos y rebotes. La misma preocupación de correo saliente duplicada en tu pila.
Y en hosts en la nube que bloquean SMTP saliente — DigitalOcean, AWS Lightsail, Linode, Vultr — las aplicaciones que solo usan SMTP no funcionan sin una solución alternativa.
Presentamos Posthorn
Posthorn es el puente. Un contenedor, una configuración, un conjunto de credenciales. Tus aplicaciones apuntan a Posthorn. Posthorn se comunica con tu proveedor.
Proporciona tres formas de entrada:
- Formulario HTTP (formularios de contacto, registros, webhooks de alerta) — Honeypot + Origen/Referer + límite de velocidad + CSRF opcional; plantilla el correo; envía
- Modo API HTTP (workers, cron, manejadores de pago, servicios internos) — Autenticación
Authorization: Bearer; cuerpo JSON; reintentos idempotentes;to_overridepor solicitud para envíos transaccionales - Receptor SMTP (Ghost, Gitea, Mastodon, Matrix, NextCloud, Authentik, cualquier cosa que emita SMTP) — AUTH PLAIN o certificado de cliente; STARTTLS requerido; listas permitidas de remitente y destinatario; analiza MIME; reenvía a través del transporte de API HTTP
Las tres entradas convergen en un transport.Message y un proveedor saliente — elige entre Postmark, Resend, Mailgun, AWS SES o un relé SMTP saliente.
Lo que Posthorn No Es
Para ahorrarte un giro equivocado:
| Lo que hace | Mira en su lugar |
|---|---|
| No es un servidor de correo — Sin almacenamiento de buzones, sin IMAP/JMAP, sin gestión de claves DKIM, sin destino MX | Stalwart, Mailcow, iRedMail |
| No es su propia infraestructura saliente — Posthorn retransmite a través de un proveedor que elegiste; no ejecuta su propia flota SMTP ni gestiona la reputación IP | Postal, Hyvor Relay |
| No es una plataforma de correo de marketing — Sin gestión de listas, sin segmentación, sin panel de campañas | Listmonk |
| No es webmail / interfaz de buzón — Sin interfaz para leer correo | Roundcube, Snappymail (con un servidor de correo) |
El nicho es la capa de integración entre tus aplicaciones autogestionadas y el proveedor transaccional que ya has elegido.
Inicio Rápido (Docker)
# docker-compose.yml
services:
posthorn:
image: ghcr.io/craigmccaskill/posthorn:latest
restart: unless-stopped
volumes:
- ./posthorn.toml:/etc/posthorn/config.toml:ro
environment:
POSTMARK_API_KEY: ${POSTMARK_API_KEY}
ports:
- "127.0.0.1:8080:8080" # enlazar a loopback; proxy inverso desde tu puerta principal
# posthorn.toml
[[endpoints]]
path = "/api/contact"
to = ["[email protected]"]
from = "Formulario de Contacto <[email protected]>"
honeypot = "_gotcha"
allowed_origins = ["https://example.com"]
required = ["name", "email", "message"]
subject = "Contacto de {{.name}}"
body = """
De: {{.name}} <{{.email}}>
{{.message}}
"""
redirect_success = "/thank-you"
[endpoints.transport]
type = "postmark"
[endpoints.transport.settings]
api_key = "${env.POSTMARK_API_KEY}"
[endpoints.rate_limit]
count = 5
interval = "1m"
Proxy inverso /api/contact desde tu puerta principal (Caddy, nginx, Traefik) a http://posthorn:8080. Apunta la acción de tu formulario a /api/contact. Hecho.
Modo API (Servidor a Servidor)
Para Workers, trabajos cron, servicios internos — cualquier cosa que hable JSON en lugar de formularios:
[[endpoints]]
path = "/api/transactional"
to = ["[email protected]"]
from = "TuApp <[email protected]>"
auth = "api-key"
api_keys = ["${env.WORKER_KEY_PRIMARY}", "${env.WORKER_KEY_BACKUP}"]
required = ["subject_line", "message"]
subject = "{{.subject_line}}"
body = "{{.message}}"
[endpoints.transport]
type = "postmark"
[endpoints.transport.settings]
api_key = "${env.POSTMARK_API_KEY}"
curl -X POST https://posthorn.yourdomain.com/api/transactional \
-H "Authorization: Bearer $WORKER_KEY_PRIMARY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reset:user-123:$(date -u +%FT%H)" \
--data '{
"to_override": "[email protected]",
"subject_line": "Restablece tu contraseña",
"message": "Haz clic aquí: https://app.example.com/reset/abc"
}'
Receptor SMTP (Ghost / Gitea / Mastodon / Authentik)
Para aplicaciones que hablan SMTP de forma nativa y no pueden reconfigurarse para llamar a una API HTTP:
[smtp_listener]
listen = ":2525"
require_tls = true
tls_cert = "/etc/posthorn/cert.pem"
tls_key = "/etc/posthorn/key.pem"
auth_required = "smtp-auth"
allowed_senders = ["*@yourdomain.com"]
max_recipients_per_session = 10
max_message_size = "1MB"
[[smtp_listener.smtp_users]]
username = "ghost"
password = "${env.GHOST_SMTP_PASSWORD}"
[smtp_listener.transport]
type = "postmark"
[smtp_listener.transport.settings]
api_key = "${env.POSTMARK_API_KEY}"
Apunta la configuración SMTP de Ghost (o cualquier aplicación) a posthorn.yourdomain.com:2525 con el nombre de usuario/contraseña anteriores. Posthorn analiza el MIME, construye un transport.Message, reenvía a través de Postmark.
Elegir un Transporte
| Transporte | Mejor para | Autenticación | Cuerpo |
|---|---|---|---|
| Postmark | Correo transaccional, valores predeterminados sólidos de entregabilidad | X-Postmark-Server-Token | JSON |
| Resend | API HTTP moderna, panel amigable para desarrolladores | Authorization: Bearer | JSON |
| Mailgun | Transaccional de mayor volumen, regiones EE. UU. + UE | HTTP Basic | multipart/form-data |
| AWS SES | Implementaciones nativas de AWS, más barato en volumen | AWS SigV4 (personalizado) | JSON |
| SMTP saliente | Cualquier relé compatible con STARTTLS (Mailtrap, tu smarthost Postfix, etc.) | AUTH PLAIN | SMTP DATA |
Cambiar de proveedor es una edición de TOML — cada transporte implementa la misma interfaz Transport.
Lista de Verificación de Producción
Antes de dirigir tráfico real a Posthorn:
- DNS — Registros SPF, DKIM y DMARC en tu dominio de envío. Sin estos, tu correo va a spam.
- Proxy inverso — Posthorn no termina TLS. Ejecútalo detrás de Caddy, nginx o Traefik.
allowed_origins(puntos finales en modo formulario) — configúralo para bloquear envíos a tu dominio. Sin esto, cualquiera puede hacer POST a tu punto final.rate_limit— establece un bucket ajustado por punto final (5/minuto es un valor predeterminado sensato para un formulario de contacto público; el modo API limita la velocidad por clave coincidente).trusted_proxies— si está detrás de un proxy inverso, enumera su CIDR (o usa el preset nombradocloudflare) para que el limitador de velocidad vea la IP real del cliente./healthzy/metrics— registrados automáticamente en el mismo receptor. Conecta tu healthcheck de Docker o scrape de Prometheus a estos.
Qué hay en v1.0
| Bloque | Detalle |
|---|---|
| Entrada de formulario | Cuerpos codificados en formulario + multipart; honeypot, Origin/Referer fail-closed, límite de velocidad, tokens CSRF opcionales |
| Modo API | auth = "api-key" con tokens Bearer (comparación en tiempo constante); tipo de contenido JSON; claves de idempotencia (24h, LRU en memoria); to_override por solicitud |
| Transportes | Postmark, Resend, Mailgun, AWS SES (SigV4 personalizado), relé SMTP saliente |
| Receptor SMTP | Receptor TCP con AUTH PLAIN / certificado de cliente, STARTTLS requerido, listas permitidas de remitente y destinatario, límite de tamaño, MIME → transport.Message |
| Operaciones | /healthz, /metrics (exposición Prometheus), modo de simulación, eliminación de IP, presets nombrados de trusted_proxies (Cloudflare) |
| Manejo de fallos | 1 reintento en transitorios/5xx (1s), 1 reintento en 429 (5s), tiempo de espera máximo de 10s |
| Registro | JSON estructurado; ID de envío UUIDv4 e ID de sesión SMTP; transport_message_id en submission_sent |
| Implementación | Binario único de Go, imagen Docker multi-arquitectura distroless en ghcr.io/craigmccaskill/posthorn |
Tres dependencias externas de Go en todo el módulo: analizador TOML, biblioteca UUID, caché LRU. Cada transporte es personalizado — sin SDK de proveedor en el código de transporte.
Hoja de Ruta
- v2 — madurez de plataforma. Registro de envíos SQLite, cola de reintentos entre reinicios, lista de supresión (automática en rebotes duros), idempotencia duradera, devoluciones de llamada de eventos de ciclo de vida mediante webhook firmado con HMAC, cancelación de suscripción con un clic RFC 8058, archivos adjuntos, cuerpo HTML, múltiples salidas por punto final (correo + webhook + registro de difusión), enrutamiento SMTP multiinquilino.
- v3 — especulativo. Interfaz de administración, desafío de spam de prueba de trabajo, cifrado PGP. Depende de la tracción de la comunidad.
Compilar desde el Código Fuente
Requiere Go 1.25+.
git clone https://github.com/craigmccaskill/posthorn
cd posthorn/core
go build -o /tmp/posthorn ./cmd/posthorn
/tmp/posthorn version
Conclusión
Posthorn resuelve un problema real para los operadores autogestionados: la fragmentación de la integración del correo saliente. Al proporcionar una puerta de enlace única y unificada con múltiples formas de entrada y backends de transporte, simplifica drásticamente tu infraestructura de correo electrónico. Un contenedor, una configuración, un conjunto de credenciales — y listo.