Posthorn: セルフホストプロジェクトのための統一されたアウトバウンドメールレイヤー
Posthornは、セルフホスト型のメールゲートウェイで、アプリからのアウトバウンドメールをPostmark、Resend、AWS SESなどのトランザクションプロバイダーに統合します。
問題: セルフホスターにとってアウトバウンドメールは混乱の元
2026年になっても、誰もメールサーバーを運用したくありません。セルフホストの運用者は、Postmark、Resend、Mailgun、AWS SESを使用しています。これらは安価で、配信性を適切に処理し、SPF/DKIM/DMARC/バウンス/送信者レピュテーションを誰かが代わりに管理してくれるからです。
しかし、セルフホストするすべてのアプリは、そのサービスと個別に統合する必要があります。お問い合わせフォーム、Ghostブログの管理メール、Giteaのマジックリンク、Mastodonの通知、リンクがクリックされたときにパスワードリセットメールを送信するCloudflare Worker。それぞれにAPIキーのコピー、独自の統合コード、リトライやバウンス処理に関する独自の癖が必要です。同じアウトバウンドの関心事がスタック全体で重複しています。
そして、DigitalOcean、AWS Lightsail、Linode、Vultrなど、アウトバウンドSMTPをブロックするクラウドホストでは、SMTPのみのアプリは回避策なしではまったく動作しません。
Posthornの登場
Posthornは橋渡し役です。1つのコンテナ、1つの設定、1組の認証情報。アプリはPosthornを指します。Posthornがプロバイダーと通信します。
3つのイングレス形状を提供します:
- HTTPフォーム (お問い合わせフォーム、サインアップ、アラートウェブフック) — ハニーポット + Origin/Referer + レート制限 + オプションのCSRF; メールをテンプレート化; 送信
- HTTP APIモード (ワーカー、cron、支払いハンドラー、内部サービス) —
Authorization: Bearer認証; JSONボディ; 冪等リトライ; トランザクション送信用のリクエストごとのto_override - SMTPリスナー (Ghost、Gitea、Mastodon、Matrix、NextCloud、Authentik、SMTPを発行するもの) — AUTH PLAINまたはクライアント証明書; STARTTLS必須; 送信者 + 受信者許可リスト; MIMEを解析; HTTP APIトランスポート経由で転送
3つのイングレスすべてが1つのtransport.Messageと1つのアウトバウンドプロバイダーに収束します — Postmark、Resend、Mailgun、AWS SES、またはアウトバウンドSMTPリレーから選択します。
Posthornではないもの
誤った方向に進まないように:
| これを行うもの | 代わりにこちらを参照 |
|---|---|
| メールサーバーではない — メールボックスストレージなし、IMAP/JMAPなし、DKIMキー管理なし、MXターゲットなし | Stalwart、Mailcow、iRedMail |
| 独自のアウトバウンドインフラではない — Posthornは選択したプロバイダーを経由してリレーします。独自のSMTPフリートを運用したり、IPレピュテーションを管理したりしません | Postal、Hyvor Relay |
| マーケティングメールプラットフォームではない — リスト管理なし、セグメンテーションなし、キャンペーンダッシュボードなし | Listmonk |
| Webメール/メールボックスUIではない — メールを読むためのインターフェースなし | Roundcube、Snappymail (メールサーバーと併用) |
その役割は、セルフホストアプリと既に選択したトランザクションプロバイダーとの間の統合レイヤーです。
クイックスタート (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" # ループバックにバインド; フロントドアからリバースプロキシ
# posthorn.toml
[[endpoints]]
path = "/api/contact"
to = ["[email protected]"]
from = "Contact Form <[email protected]>"
honeypot = "_gotcha"
allowed_origins = ["https://example.com"]
required = ["name", "email", "message"]
subject = "Contact from {{.name}}"
body = """
From: {{.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"
フロントドア (Caddy、nginx、Traefik) から /api/contact を http://posthorn:8080 にリバースプロキシします。フォームのアクションを /api/contact に向けます。完了です。
APIモード (サーバー間)
ワーカー、cronジョブ、内部サービス — フォームではなくJSONを話すもの:
[[endpoints]]
path = "/api/transactional"
to = ["[email protected]"]
from = "YourApp <[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": "Reset your password",
"message": "Click here: https://app.example.com/reset/abc"
}'
SMTPリスナー (Ghost / Gitea / Mastodon / Authentik)
ネイティブにSMTPを話し、HTTP APIを呼び出すように再設定できないアプリ向け:
[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}"
Ghost (または任意のアプリのSMTP設定) を posthorn.yourdomain.com:2525 に、上記のユーザー名/パスワードで向けます。PosthornがMIMEを解析し、transport.Messageを構築し、Postmark経由で転送します。
トランスポートの選択
| トランスポート | 最適な用途 | 認証 | ボディ |
|---|---|---|---|
| Postmark | トランザクションメール、強力な配信性デフォルト | X-Postmark-Server-Token | JSON |
| Resend | 最新のHTTP API、開発者フレンドリーなダッシュボード | Authorization: Bearer | JSON |
| Mailgun | 高ボリュームのトランザクション、米国 + EUリージョン | HTTP Basic | multipart/form-data |
| AWS SES | AWSネイティブデプロイ、ボリュームあたり最も安価 | AWS SigV4 (独自) | JSON |
| アウトバウンドSMTP | STARTTLS対応リレー (Mailtrap、Postfixスマートホストなど) | AUTH PLAIN | SMTP DATA |
プロバイダーの切り替えはTOMLの編集です — すべてのトランスポートが同じTransportインターフェースを実装しています。
本番環境のチェックリスト
実際のトラフィックをPosthornに向ける前に:
- DNS — 送信ドメインにSPF、DKIM、DMARCレコード。これらがないとメールはスパムになります。
- リバースプロキシ — PosthornはTLSを終端しません。Caddy、nginx、またはTraefikの背後で実行します。
allowed_origins(フォームモードエンドポイント) — これを設定して、送信をドメインにロックします。これがないと、誰でもエンドポイントにPOSTできます。rate_limit— エンドポイントごとにタイトなバケットを設定します (公開お問い合わせフォームでは5/分が適切なデフォルトです。APIモードでは、一致したキーごとにレート制限されます)。trusted_proxies— リバースプロキシの背後にある場合、そのCIDRをリストします (またはcloudflareという名前のプリセットを使用)。これにより、レートリミッターが実際のクライアントIPを認識します。/healthzと/metrics— 同じリスナーに自動登録されます。DockerヘルスチェックまたはPrometheusスクレイプをこれらに配線します。
v1.0の内容
| ブロック | 詳細 |
|---|---|
| フォームイングレス | フォームエンコード + マルチパートボディ; ハニーポット、Origin/Refererフェイルクローズド、レート制限、オプションのCSRFトークン |
| APIモード | auth = "api-key"とBearerトークン (定数時間比較); JSONコンテンツタイプ; 冪等キー (24時間、インメモリLRU); リクエストごとのto_override |
| トランスポート | Postmark、Resend、Mailgun、AWS SES (独自SigV4)、アウトバウンドSMTPリレー |
| SMTPリスナー | AUTH PLAIN / クライアント証明書によるTCPリスナー、STARTTLS必須、送信者 + 受信者許可リスト、サイズ制限、MIME → transport.Message |
| 運用 | /healthz、/metrics (Prometheus公開)、ドライランモード、IPストリッピング、名前付きtrusted_proxiesプリセット (Cloudflare) |
| 障害処理 | 一時的/5xxで1回リトライ (1秒)、429で1回リトライ (5秒)、10秒のハードタイムアウト |
| ロギング | 構造化JSON; UUIDv4送信IDとSMTPセッションID; submission_sent内のtransport_message_id |
| デプロイ | 単一のGoバイナリ、マルチアーキテクチャdistroless Dockerイメージ (ghcr.io/craigmccaskill/posthorn) |
モジュール全体で3つの外部Go依存関係: TOMLパーサー、UUIDライブラリ、LRUキャッシュ。すべてのトランスポートは独自実装です — トランスポートコードにベンダーSDKはありません。
ロードマップ
- v2 — プラットフォームの成熟。SQLite送信ログ、再起動をまたぐリトライキュー、抑制リスト (ハードバウンス時に自動)、耐久性のある冪等性、HMAC署名付きウェブフックによるライフサイクルイベントコールバック、RFC 8058ワンクリック購読解除、ファイル添付、HTMLボディ、エンドポイントごとの複数出力 (メール + ウェブフック + ログファンアウト)、マルチテナントSMTPルーティング。
- v3 — 推測。管理UI、プルーフオブワークスパムチャレンジ、PGP暗号化。コミュニティの牽引力に依存します。
ソースからビルド
Go 1.25+が必要です。
git clone https://github.com/craigmccaskill/posthorn
cd posthorn/core
go build -o /tmp/posthorn ./cmd/posthorn
/tmp/posthorn version
結論
Posthornは、セルフホスト運用者にとっての真の痛点、つまりアウトバウンドメール統合の断片化を解決します。複数のイングレス形状とトランスポートバックエンドを備えた単一の統合ゲートウェイを提供することで、メールインフラを劇的に簡素化します。1つのコンテナ、1つの設定、1組の認証情報 — これで完了です。