無償版G Suiteが廃止されるのでAWS SESとDockerでメール送受信システムを作った

20日木曜、大きなニュースが飛び込んできました。2012年に提供が終了して、そのまま9年近く利用できていた旧無償版G Suiteが7月1日に廃止されてしまいます。

www.itmedia.co.jp

自分も黎明の頃から勉強会のメールアドレスや、友人に独自ドメインアドレスを提供したりと、便利に使わせてもらってきていたのですが、現状の利用規模的に個人ユースで月数千円の追加支出になるのはさすがに許容できません。既にGoogle OneやYoutube PremiumなどGoogleにはかなりお金を払っているので、これ以上Googleにお金取られるのは気分的にヤダというのもあります…。

そこでまじめに検討した結果、独自にメール受信システムを作ることにしました。

結果、月々100円以内におさまるシステムに仕上がりました。

2022/05/05(Thr) 追記

本記事では当初Oracle Cloud Infrastructureを使って使用していましたが、課金状態にあるにも関わらず突如理由もなくアカウントを停止されるという事態が発生しているとのことです。

zenn.dev

幸い僕の手元の環境は問題ない状態でしたが、このような状況では安心して使いつづけられませんので、OCIのコンピュートで建てていたVMAWS Lightsailに移しました。 それに伴い本記事は改題しておりますが、中の説明は引き続きOCIとなっています。

これにより、3.5ドル/月が加算され、トータルでは500円程度の維持費となりそうです。

要件検討

まず、要件を洗い出しました。

  • 独自ドメインのメールアドレスをいくつでも作れる。
  • + を使ったエイリアス(ex. user+test@example.com)を使える。
  • 受信周りのインフラがきちんと管理されていること。
    • インフラが死んでる間にメールを取りこぼすのは勘弁
    • この時点で独自に完全なメールサーバーを組むのはなし
  • 出せても税込300円/月くらい。

これを満たせるメールサービスはありませんでした。ぐぬぬ + エイリアスに完全ロックインですね…。

ちなみに、エイリアス不要でよければ、iCloud+(税込130円/月、独自ドメインメールアドレス5個まで)やさくらのメールボックス(税抜87円/月、メールアドレス数制限なし)で良さそうです。

とはいえこのままGoogle Workspaceにお金払いたくない…という気持ちが強くなっていき…今はサーバーレスの世の中だしインフラ管理の部分あまり気にしなくてもいける感じになるんじゃないか?サーバーレスなら安く上げられるんじゃないか?という考えで作ることを検討してみることにしました。

実装の検討

まず、考えたのはこういう構成です。

AWSのSESでメールを受信、GmailAPIGmailのメールボックスに直接配信します。 SESでメールを受信すると来たメールをS3のバケットに保存して、SNSに通知を送ってくれます。それを受けて無料版のGmailAPIでメールを配信することです。

Gmailにはusers.messages.importというAPIがあり、これを使うことで通常のスパムフィルタなどを機能しつつメールボックスに配信されます。SESで受信してS3に保存されたメールの内容を、このAPIに流し込めばそのままGmailの受信トレイに届き、新着通知も出ることも確認しました。 ただし、ユーザーが定義した振り分けがされないという問題点はあります。が、既にいろいろ整理してきているので、自分としては特に問題にならないと判断しました。

しかし、この方法には問題点がありました。APIを使うのでAPIのOAuthを通します。常時使うのでリフレッシュトークンを使ったトークン更新が必要です。個人的に使うものですし、テスト環境で使いつづけようと思ったところ、テスト環境のリフレッシュトークンは有効期限が7日間と異常に短く、これでは使い物になりません。

developers.google.com

審査を通すレベルまでサービスを作り込む余裕はありませんし、もし作ったとしてもGoogleのOAuthの審査はかなり厳しいとも聞きます。また、内容的にも審査通らなさそうです。

そこで、もう少し考えました。 POP3サーバーを自力で書くのは、サーバーライブラリを使ったとしても仕様的にかなり面倒そう。 そもそもソケットを使わないといけなくなる時点で、FaaSやPaaSを使ったサーバーレスにはできなくなり、インスタンスサーバーがどうしても必要になってしまう。

では、もうインスタンスサーバー使う前提で考えて、一般のメールサーバーソフトウェアで使えるものは使う。ただメールの保管ストレージだけは安価で安定するものを使いつつ、簡単に復旧できるようにする。

そうしてできたのがこの構成。

  • インスタンスサーバー側にメールを受け取るAPIを用意して、このAPIが一般的なメール配送サーバーに配信。
  • 配信サーバーはDovecotを選択。
  • 最終的に、GmailPOP3Dovecotにメールを受信しに行き、Gmailで見る。
  • GmailのPOPは受信に時間がかかるので、受信までの間にすぐに確認したい場合(例えば、メールアドレスの確認リンクが送られてくる場合)は、macOS/iPhoneのメールから、IMAPDovecotを直接参照する。
  • もしインスタンスサーバーがダウンした場合も、AWSのサーバーレス構成内で失敗状態で保持して、必要に応じて再送できるようできる。

Dovecotへの配信部分はいろいろ調べた結果、LMTPという、SMTPをローカルメール用にしたプロトコルが使われます。 LMTPのメッセージ本文として、SESがS3に保存した内容をそのまま流し込めばDovecotへのメール配信が成立します。 LMTPを実装したライブラリはなかなかないのですが、Go言語のgo-smtpがありましたので、そのままHTTP側も含めてGo言語でAPIを実装しました。

インスタンスサーバーも無料にしたい: Oracle Cloud Infrastructureの無料枠がすごかった

さて、インスタンスサーバーが必要となり、新しくもう1台VPSを用意しないといけなかったのですが、ここで費用を掛けてしまうと費用の制約にあたってしまいます。 Google Cloud EngineのAlways Free枠のは既に使ってしまっているので、何か他にないかと思ったら、Oracle Cloud Infrastructureでも無料でVMを作れることがわかったので、これを使うことにしました。

www.oracle.com

というか、1GB RAMのx86_64のVMを2台と、24GBメモリを分割して最大4台のARMのVMを建てられるとか相当太っ腹でびっくりしました…。Oracleというのがネックだけど。

東京だとVMがなかなか作れないということでしたが、SESのメール受信はリージョンが限定されていてもとより東京では作れなかったので、AWS側のリージョンに近いリージョンをホームリージョンにしました。

最終的なシステム構成

最終的にはこういう構成にしました。

  • SESでメールを受信したら、Lambdaで配信する先を決定して、DynamoDBに配信情報を書き込み。同時にSQSに配信リクエストを送信。
    • 受信メールアドレスと配信先のマッピングは、設定情報としてDynamoDBに入っている。
  • SQSの配信リクエストを受けて、配信LambdaがOracle Cloud VMに建てたLMTPプロキシAPI(lmtpdeliver)を呼び出し
    • 配信の成否をDynamoDBにあわせて書き込み
  • LMTPプロキシAPIDovecotのLMTPにメールを配信。Dovecotはメールボックスに保存。
    • このメールボックスには、S3をマウントして使用。ファイルの日時属性などが必要になるため、S3などのオブジェクトストレージにファイルシステムを構築するS3QLを使用。

メールの通常利用は通常版のGmailを引き続き利用。Google Oneは払ってるし長期保存も多少安心でしょう。即時に確認したい場合はIMAPで見ます。

また、構築にあたってはIaaCを意識して、AWS上のサービスはAWS CDKでデプロイしています。更に、VM上のサービスはdocker-composeで構築。VMはダウンの恐れがありますが、認証情報等の設定ファイルだけ残しておけば、docker-compose upして、DNSのCNAMEを書き換えれば復活できます。

更に、この図にはありませんが、SESの送信制限解除をして、DovecotにSubmission Proxyを指定してあり、メール受信と同一のサーバーに接続し、受信と同じ認証情報を使って送信することも可能にしました。

無限エイリアスの統合

メールの送信先をLambdaでDynamoDBに書き込まれた定義情報を元に処理できるようにしたので、以前作った無限エイリアスの仕組みもあわせてここに組み込むことができるようになりました。

  • user+any@example.com 等の + エイリアスuser@example.com に配信。
  • @user.example.com 等、特定のドメインへの配信は user@example.com に全部配信。
    • これで google@user.example.comamazon@user.example.com 等、サービス毎にメールアドレスを変更可能。

また、以前の仕組みでは転送することによってFrom欄が変わってしまう問題や、undisclosure-receipientsの場合に配信されない問題などもありましたが、メールを直接Gmailに受信させることができるようになったのでこの問題も解消することができています。

実装にあたってはまったこと

AWSのサービスやDockerなどは、仕事でも使ってきたのでそこまでひっかからないだろうと思ったのですが、時間がかかったのがS3QLやOCIなどの部分。

S3QLの認証情報が正しくセットされない

S3QLの利用にあたっては、こちらのコンテナを使わせてもらいました。

github.com

しかし、環境変数を使った方法ではS3に接続することができませんでした。ログの出力的にどうもバグっぽいです。事前にauthfileを作って読み込ませれば正しく認証情報がセットされ起動できました。

S3QLはvolumeでは共有できない

ファイルシステムの動作に依存しているのか、volumeでは共有できませんでした。bindを使う必要がありました。

S3QLでマウントしたパスをroot以外で読めない

今回一番解決に時間がかかった問題です。

S3QLでマウントしたパスを、root以外が読むことができませんでした。Dovecotはrootでは動作しないように作られていて、vmailユーザーで読める必要があります。

パーミッションd?????????になってて、本当に原因がわかりませんでした…。

これは、Docker側というよりFUSE側の問題だったようで、S3QL_MOUNT_OPTIONS--allow-otherをつけることで解決できました。他のFUSEファイルシステムでもある問題らしいです。

Oracle Cloud InfrastrucureのVMに対する固定IPアドレス付与

Oracle Cloud での固定IP付与がわからないまま試していたところ、再起動しても変わらないっぽいしいける?と思ってたところ、後から調べてたらありました。

qiita.com

月々どれくらいかかるのか

で、最終的に料金がどうなったかですが、これまでの無限エイリアスの運用実績を加味しても、恐らくめちゃくちゃ安く上がると思います。

  • Oracle Cloud Infrastructureで建てたVMは無料
  • LambdaをはじめAWSの各種サービスはだいたい無料枠におさまる
  • SESの料金は更に最初の1000通までは無料枠で、それを超えると受信1000通あたり0.10ドルなので、メールの数が多いと少しかかる
  • S3の料金もちょっとだけかかる。

僕の使い方(月々1000通以下の受信)であれば、恐らくだいたい数円〜数十円の範囲でおさまると思います。

実装の公開(途中)

まだ一通り動作が繋がったところですが、リポジトリを公開しておきます。 まだ実装直後で自分でもテスト中ですので、サンプルと捉えていただければと思います。

(AWS側、メール転送エージェント(MTA)) github.com

(OCI側、メール配送エージェント(MDA)) github.com

(LMTP配信API) github.com