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/contacthttp://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組の認証情報 — これで完了です。

ソース

craigmccaskill/posthorn: アプリとトランザクションメールプロバイダー (Postmark、Resend、Mailgun、AWS SES、またはアウトバウンドSMTP) の間のセルフホスト型メールゲートウェイ。3つのイングレス形状 (HTTPフォーム、HTTP API、SMTP)。1つのDockerコンテナ、1つのTOML設定。