Posthorn : La couche de messagerie sortante unifiée pour les projets auto-hébergés
Posthorn est une passerelle de messagerie auto-hébergée qui unifie les courriels sortants de vos applications vers des fournisseurs transactionnels comme Postmark, Resend et AWS SES.
Le problème : La messagerie sortante est un casse-tête pour les auto-hébergeurs
Personne ne veut gérer un serveur de messagerie en 2026. Les opérateurs auto-hébergés utilisent Postmark, Resend, Mailgun ou AWS SES parce qu'ils sont bon marché, qu'ils gèrent correctement la délivrabilité et que quelqu'un d'autre s'occupe des SPF/DKIM/DMARC/rebonds/réputation de l'expéditeur.
Mais chaque application que vous auto-hébergez doit s'intégrer indépendamment à ce service. Votre formulaire de contact. Les e-mails d'administration de votre blog Ghost. Vos liens magiques Gitea. Vos notifications Mastodon. Le worker Cloudflare qui envoie un e-mail de réinitialisation de mot de passe lorsque quelqu'un clique sur le lien. Chacun nécessite sa propre copie de la clé API, son propre code d'intégration, ses propres particularités concernant les tentatives et la gestion des rebonds. La même préoccupation de messagerie sortante dupliquée dans toute votre pile.
Et sur les hébergeurs cloud qui bloquent le SMTP sortant — DigitalOcean, AWS Lightsail, Linode, Vultr — les applications SMTP uniquement ne fonctionnent pas du tout sans une solution de contournement.
Voici Posthorn
Posthorn est le pont. Un conteneur, une configuration, un ensemble d'identifiants. Vos applications pointent vers Posthorn. Posthorn communique avec votre fournisseur.
Il propose trois formes d'entrée :
- Formulaire HTTP (formulaires de contact, inscriptions, webhooks d'alerte) — Honeypot + Origin/Référent + limite de débit + CSRF optionnel ; modèle l'e-mail ; envoie
- Mode API HTTP (workers, cron, gestionnaires de paiement, services internes) — Authentification
Authorization: Bearer; corps JSON ; tentatives idempotentes ;to_overridepar requête pour les envois transactionnels - Écouteur SMTP (Ghost, Gitea, Mastodon, Matrix, NextCloud, Authentik, tout ce qui émet du SMTP) — AUTH PLAIN ou certificat client ; STARTTLS requis ; listes d'autorisation d'expéditeur et de destinataire ; analyse MIME ; transfert via transport API HTTP
Les trois entrées convergent vers un transport.Message et un fournisseur sortant — choisissez parmi Postmark, Resend, Mailgun, AWS SES ou un relais SMTP sortant.
Ce que Posthorn n'est pas
Pour vous éviter une erreur :
| Ce qu'il fait | Regardez plutôt |
|---|---|
| Pas un serveur de messagerie — Pas de stockage de boîtes aux lettres, pas d'IMAP/JMAP, pas de gestion de clés DKIM, pas de cible MX | Stalwart, Mailcow, iRedMail |
| Pas sa propre infrastructure sortante — Posthorn relaie via un fournisseur que vous avez choisi ; il ne gère pas sa propre flotte SMTP ni la réputation IP | Postal, Hyvor Relay |
| Pas une plateforme d'e-mail marketing — Pas de gestion de listes, pas de segmentation, pas de tableau de bord de campagne | Listmonk |
| Pas un webmail / une interface de boîte aux lettres — Pas d'interface pour lire les e-mails | Roundcube, Snappymail (avec un serveur de messagerie) |
Le créneau est la couche d'intégration entre vos applications auto-hébergées et le fournisseur transactionnel que vous avez déjà choisi.
Démarrage rapide (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" # lier au loopback ; proxy inverse depuis votre porte d'entrée
# posthorn.toml
[[endpoints]]
path = "/api/contact"
to = ["[email protected]"]
from = "Formulaire de contact <[email protected]>"
honeypot = "_gotcha"
allowed_origins = ["https://exemple.com"]
required = ["nom", "email", "message"]
subject = "Contact de {{.nom}}"
body = """
De : {{.nom}} <{{.email}}>
{{.message}}
"""
redirect_success = "/merci"
[endpoints.transport]
type = "postmark"
[endpoints.transport.settings]
api_key = "${env.POSTMARK_API_KEY}"
[endpoints.rate_limit]
count = 5
interval = "1m"
Proxy inverse /api/contact depuis votre porte d'entrée (Caddy, nginx, Traefik) vers http://posthorn:8080. Pointez l'action de votre formulaire vers /api/contact. Terminé.
Mode API (Serveur à serveur)
Pour les Workers, les tâches cron, les services internes — tout ce qui parle JSON au lieu de formulaires :
[[endpoints]]
path = "/api/transactionnel"
to = ["[email protected]"]
from = "VotreApp <[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.votredomaine.com/api/transactionnel \
-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": "Réinitialisez votre mot de passe",
"message": "Cliquez ici : https://app.exemple.com/reset/abc"
}'
Écouteur SMTP (Ghost / Gitea / Mastodon / Authentik)
Pour les applications qui parlent SMTP nativement et ne peuvent pas être reconfigurées pour appeler une 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 = ["*@votredomaine.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}"
Pointez Ghost (ou la configuration SMTP de n'importe quelle application) vers posthorn.votredomaine.com:2525 avec le nom d'utilisateur/mot de passe ci-dessus. Posthorn analyse le MIME, construit un transport.Message, transfère via Postmark.
Choisir un transport
| Transport | Idéal pour | Authentification | Corps |
|---|---|---|---|
| Postmark | E-mail transactionnel, bonnes pratiques de délivrabilité par défaut | X-Postmark-Server-Token | JSON |
| Resend | API HTTP moderne, tableau de bord convivial pour les développeurs | Authorization: Bearer | JSON |
| Mailgun | Transactionnel à volume élevé, régions US + UE | HTTP Basic | multipart/form-data |
| AWS SES | Déploiements natifs AWS, le moins cher en volume | AWS SigV4 (sur mesure) | JSON |
| SMTP sortant | Tout relais compatible STARTTLS (Mailtrap, votre smarthost Postfix, etc.) | AUTH PLAIN | SMTP DATA |
Changer de fournisseur est une modification du TOML — chaque transport implémente la même interface Transport.
Liste de contrôle pour la production
Avant de diriger du trafic réel vers Posthorn :
- DNS — Enregistrements SPF, DKIM et DMARC sur votre domaine d'envoi. Sans cela, vos e-mails vont dans les spams.
- Proxy inverse — Posthorn ne termine pas TLS. Exécutez-le derrière Caddy, nginx ou Traefik.
allowed_origins(points de terminaison en mode formulaire) — définissez ceci pour verrouiller les soumissions à votre domaine. Sans cela, n'importe qui peut POSTER vers votre point de terminaison.rate_limit— définissez un bucket serré par point de terminaison (5/minute est une valeur par défaut raisonnable pour un formulaire de contact public ; le mode API limite le débit par clé correspondante).trusted_proxies— si derrière un proxy inverse, listez son CIDR (ou utilisez le préréglage nommécloudflare) pour que le limiteur de débit voie la véritable adresse IP du client./healthzet/metrics— enregistrés automatiquement sur le même écouteur. Connectez votre healthcheck Docker ou votre scraping Prometheus à ceux-ci.
Ce qui est dans v1.0
| Bloc | Détail |
|---|---|
| Entrée formulaire | Corps encodés en formulaire + multipart ; honeypot, Origin/Référent fermé en cas d'échec, limite de débit, jetons CSRF optionnels |
| Mode API | auth = "api-key" avec jetons Bearer (comparaison en temps constant) ; type de contenu JSON ; clés d'idempotence (24h, LRU en mémoire) ; to_override par requête |
| Transports | Postmark, Resend, Mailgun, AWS SES (SigV4 sur mesure), relais SMTP sortant |
| Écouteur SMTP | Écouteur TCP avec AUTH PLAIN / certificat client, STARTTLS requis, listes d'autorisation d'expéditeur et de destinataire, limite de taille, MIME → transport.Message |
| Opérations | /healthz, /metrics (exposition Prometheus), mode dry-run, suppression d'IP, préréglages nommés de proxies de confiance (Cloudflare) |
| Gestion des échecs | 1 tentative sur erreur transitoire/5xx (1s), 1 tentative sur 429 (5s), délai d'attente strict de 10s |
| Journalisation | JSON structuré ; identifiants de soumission UUIDv4 et identifiants de session SMTP ; transport_message_id dans submission_sent |
| Déploiement | Binaire Go unique, image Docker distroless multi-arch à ghcr.io/craigmccaskill/posthorn |
Trois dépendances Go externes dans tout le module : analyseur TOML, bibliothèque UUID, cache LRU. Chaque transport est sur mesure — pas de SDK fournisseur dans le code de transport.
Feuille de route
- v2 — maturité de la plateforme. Journal de soumission SQLite, file d'attente de tentatives après redémarrage, liste de suppression (automatique sur les rebonds durs), idempotence durable, rappels d'événements de cycle de vie via webhook signé HMAC, désabonnement en un clic RFC 8058, pièces jointes, corps HTML, sorties multiples par point de terminaison (e-mail + webhook + journal de diffusion), routage SMTP multi-locataire.
- v3 — spéculatif. Interface d'administration, défi anti-spam par preuve de travail, chiffrement PGP. Dépend de l'adoption par la communauté.
Construire à partir des sources
Nécessite Go 1.25+.
git clone https://github.com/craigmccaskill/posthorn
cd posthorn/core
go build -o /tmp/posthorn ./cmd/posthorn
/tmp/posthorn version
Conclusion
Posthorn résout un véritable problème pour les opérateurs auto-hébergés : la fragmentation de l'intégration de la messagerie sortante. En fournissant une passerelle unique et unifiée avec plusieurs formes d'entrée et des backends de transport, il simplifie considérablement votre infrastructure de messagerie. Un conteneur, une configuration, un ensemble d'identifiants — et vous avez terminé.