Emailwerk
Emailwerk は私のトランザクションメール用サービスです。netsnek.com のお問い合わせフォームの裏側でメールを送信し、適格署名が揃うまで送信を保留し、Gmail と直接やり取りします。私はこれを cronit の Nico と共同で開発し、自分の会社 Netsnek e.U. のもとで運用しています。このページは、そこに至った経緯を語ります。
3度の挑戦
Emailwerk は、2023年に mailpress として始まったプロジェクトの3番目のメジャーバージョンです。3つのバージョンを要したのは、その土台となるツールも一緒に成長してきたからでもあります。Nico は Emailwerk が動作するフレームワーク Pylon を開発しています。Pylon のメジャーバージョンが上がるたびに、新しい mailpress のバージョンが生まれてきました。
最初のバージョンは、Pylon の前身である snek-functions の上に生まれました。HTML テンプレートはコードに直接埋め込まれ、実際の送信は外部のメーラー・マイクロサービスが担っていました。多くは間に合わせでしたが、当時のアイデアのうち2つは今日まで生き残っています。1つは連鎖テンプレートで、1回の送信が後続のメールを引き起こします。たとえばチームへの問い合わせと、お客様への確認メールです。もう1つは verifyReplyTo で、他人のアドレスの名義で返信可能なメールを送ることを防ぐチェックです。
mailpress v2 は2024年、最初の本格的な一手でした。Pylon v2、Prisma と Postgres、Zitadel によるマルチテナント対応、テンプレート言語としての Twig、そして独立した Gatsby 製の管理画面。このバージョンは2年間、本番で稼働しました。しかし、今なら同じようには選ばない設計上の判断も含まれていました。それについてはすぐ後で述べます。
2026年には Pylon v3 へのリライトが来て、それとともに新しい名前が付きました。mailpress は Emailwerk になりました。社内ツールを製品にしたいからです。管理画面はサービス自体の中に移り、API と同じプ ロセスの中でサーバーサイドでレンダリングされます。さらに送信履歴と、同じ Postgres データベース上で動く本物のジョブキューが加わりました。結果として、データベース1つを持つ単一の Node プロセスになりました。フロントエンドの個別デプロイもなく、2つ目のリポジトリもなく、CORS も Redis もありません。
今、私のために何をしているか
いちばん目に見えるのは netsnek.com のお問い合わせフォームです。そこに書き込んだ人は私たちへの問い合わせを送り出し、連携したテンプレートを通じて確認メールを受け取ります。どちらもログインなしで Emailwerk の匿名経路を通りますが、それでもオープンリレーではありません。この話には専用のページがあります。
次に署名です。Emailwerk は、内容が ID Austria によって適格電子署名されるまで送信を保留できます。メールはキューに入れる時点でレンダリングされて凍結され、その後で私がウェブ上のセレモニーで署名し、それから初めて、署名済み PDF を添付して送り出されます。その下には PDF-Over の私たちによる TypeScript リライトがあり、さらに厳密な内容に対する独自の PGP レイヤーが載ります。そうしたメールを受け取った人は、signature.netsnek.com で署名を検証できます。ブラウザ上でも、オフラインでも。
そして Gmail です。送信元のメールボックスを OAuth で接続すると、Emailwerk はそのメールボックスとして Gmail API 経由で送信します。メールボックスに設定してあるシグネチャは自動的に一緒に付き、署名付きの送信ではそれも一緒に署名されます。正直に言うと、この連携にはまだ癖があります。OAuth アプリが Google 側でテストステータスのままなので、トークンが1週間ほどで失効し、私は接続を承認し直さなければなりません。
移行にあたって、私は古い mailpress インスタンスの16個のテンプレートを Emailwerk に移しました。それらが変更なしにレンダリングされるよう、Emailwerk は新規のものの標準である Liquid に加えて、互換エンジンとして Twig も残しています。個々のテンプレートの出自は、レターヘッドの日付フィルターで露見しました。Twig は知っていて Liquid は知らないフィルターです。
私が葬ったもの
このリライトは埋葬でもありました。mailpress v2 の4つの構造は、持っていきたくありませんでした。
匿名送信。 お問い合わせフォームにはログインなしの経路が必要です。Emailwerk では、その際に受信者を決めるのは常にテンプレートであって、呼び出し側ではありません。この分離は、誰かが書き忘れうるチェックルーチンではなく、データモデルの中にあります。
トランスフォーマー。 v2 では、テンプレートを小さなスクリプト部品で拡張できました。Emailwerk にはもうありません。囲いをより厳重にしたからではなく、必要そのものが消えたからです。件名、受信者、Reply-To は今やそれ自体がテンプレート文字列であり、本文と同じ変数でレンダリングされます。そのため16個のテンプレートの移行では、古い部品は意図的に置き去りにしました。
認証情報はスキーマに属さない。 Emailwerk では、SMTP パスワードと API キーは暗号化されて独自のデータモデルに置かれ、そのモデルは GraphQL API からは構造的にまったく到達できません。スキーマが知らないものは、どのリゾルバーもうっかり外に出すことはできません。
連鎖テンプレートには制限が要る。 自分自身を再び起動する連鎖は、誰も止めなければ堂々巡りになります。Emailwerk にはその手前に循環防止が置かれており、お問い合わせフォームの確認メールはちょうど1階層の深さまでです。
私が学んだこと
危険な機能はサンドボックスに入れるのではなく、不要にする。 トランスフォーマーがその教材です。その安全版は、より良い柵ではなく、それ自体がテンプレートである封筒でした。機能は消え、能力は残りました。
構造は規律に勝る。 認証ガードは書き忘れうるし、フィールドは応答にうっかり紛れ込みうる。スキーマに存在しないデータモデルは、誰も問い合わせできません。テンプレートからしか来られない受信者は、どの呼び出し側にも曲げられません。Emailwerk でもっとも信頼できるセキュリティ上の判断は、誰も毎回あらためて下す必要のないものです。
部品が少ないほど、心配も少ない。 3つのバージョンを通じて、このサービスは大きくなるのではなく、コンパクトになりました。v1 は外部のメーラーを、v2 は独自リポジトリに置かれた別の管理画面を必要としました。v3 は画面を同じプロセスでレンダリングし、キューを同じデータベースに置きます。取り除かれた部品はどれも、もう壊れようのない部品です。
リライトはゼロからのやり直しではない。 良いアイデアは引っ越します。verifyReplyTo と連鎖テンプレートは、いちばん最初のバージョンに由来します。データも引っ越します。古い Twig テンプレートのためには、わざわざ互換エンジンがあります。後に残るのは失敗だけであり、まさにそのためにリライトをするのです。
このセクションの続き
お問い合わせフォーム
netsnek.com のお問い合わせフォームがどのようにログインなしで直接 Emailwerk に送信し、それでもなぜオープンリレーにならないのか。