ActionMailerでメール送信方法を自作するためにコードを読んでみる


SendGridのWeb API v3 を使ってActionMailerでメール送信する gem を作成した時にActionMailer周りのコードを読んだのでメモ。

ちなみにActionMailerでSendGridを使う場合の公式の推奨は、SMTP APIを使う方法 と思われる。SMTP APIを使う際に気になる点として、ユーザーIDとパスワードを指定しないといけない点がある。一方、Web APIでは管理画面で発行したAPI keyを指定する。API keyは環境ごとに複数発行可能で、かつ権限をカスタマイズすることもできる。情報が漏洩した際の対応のしやすさはWeb APIの方が優っていると考え、採用したかった。

まとめ

  • #initialize, #settings, #deliver! の3つのメソッドを実装した自作メール送信クラスを作成
  • 自作クラスをRails起動時の設定か、ActionMailer::Baseを継承したクラスでセット
  • ヘッダーや本文がセットされた Mail::Message のインスタンスが #deliver! の引数に渡されるので、このインスタンスから必要な情報を集めてメール送信処理を実装すればOK

詳細

対象バージョンは以下

  • ActionMailerは「5.1.1」
  • mail gemは「2.6.5」

ActionMailerの送信処理の設定

メール送信の自作に関して、ActionMailer::BaseのAPIドキュメントの「delivery_method」にわずかながら記載がある。

http://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Configuration+options

delivery_method – Defines a delivery method. Possible values are :smtp (default), :sendmail, :test, and :file. Or you may provide a custom delivery method object e.g. MyOwnDeliveryMethodClass. See the Mail gem documentation on the interface you need to implement for a custom delivery agent.

自作のメール送信クラス MyOwnDeliveryMethodClass がある場合、Rails起動時の設定で以下のように設定すれば使用できるとのこと。

もしくは、特定のActionMailerのクラスやメソッドでのみ使用したい場合、以下のような設定をすることで使用できる。

メール送信クラスのインターフェース

MyOwnDeliveryMethodClass はMail gemのインターフェースに沿っていればOKとのこと。インターフェースに関してはドキュメントをざっと探した限り、見つけることができなかった。仕方がないので、デフォルトの送信方法であるSMTPの送信クラスからインターフェースを推測する。

  1. 1つの引数を受け取る #initialize が必要。引数の型は、その後HashのmergeにいれていることからHashが来ると考えてよさそう。
  2. #initialize でセットしたHashを返す #settings が必要。
  3. 1つの引数を受け取り実際の送信処理を行う #deliver! が必要。

ActionMailerの初期化

ActionMailer::Railtie

config/application.rbrequire 'action_mailer/railtie' しているため、まずここから確認する。

大幅に省略したが、重要なのは※1で記載した部分。先ほど設定で config.action_mailer.delivery_method = MyOwnDeliveryMethodClass としているため、ActiveSupport.run_load_hooks(:action_mailer) が実行されたタイミングで self.delivery_method = MyOwnDeliveryMethodClass が実行される。selfが何になっているかは、この後確認する。

ActionMailerの読み込み

次にActionMailer自体の読み込みを確認する。

Railsの他のモジュールと同様、各クラスがautoloadで宣言されている。developmentやtestでは初回参照時、productionでは初期化直後に読み込みが実施される。

ActionMailer::Baseの読み込み

ActionMailer::Base読み込み時の処理も確認しておく。大きなクラスなので初期化に関係する最低限の部分のみを表示。

  1. メール送信方法に関するクラスメソッド、インスタンスメソッドが追加されている。 #delivery_method, #delivery_method= がここで追加されている。
  2. :action_mailer のhookを自身をselfにして走らせる。これにより ActionMailer::Railtie でセットしたスクリプトが実行されるため、ActionMailer::Base.delivery_method = MyOwnDeliveryMethodClass が実行されている。

ActionMailer::Baseでメール送信

ここからはActionMailerのメール送信処理を見ていく。まず、以下のような自作のMailerクラスがあることを想定する。

メール送信は、以下のように呼び出すことで実行できる。

MyMailer.test_mail とその返り値に対する #deliver_now! の処理を見ていく。

ActionMailer::Base.method_missing

MyMailer のクラスメソッドに .test_mail はない。そのため .method_missing が呼び出されることになる。ActionMailer::Base.method_missing が定義されている。

#action_methodsAbstractController::Base で定義されているメソッドで、基本的にはpublicなインスタンスメソッドの一覧を返すと考えてよい。
test_mailMyMailer クラスにpublicで定義されている、そのため method_missing では ActionMailer::MessageDelivery のインスタンスを生成し返している。

ActionMailer::MessageDelivery#deliver_now!

  1. MyMailer インスタンスを1つ生成
  2. AbstractController::Base#process を呼ぶ。いくつかのメソッド呼び出しを経て、 MyMailer#test_mail を呼び出す。このメソッドでは ActionMailer::Base#mail を呼び出している。次に確認する。
  3. #handle_exceptions はエラー処理を追加しているだけで、このインスタンスをselfとしてブロックを実行している。 #message で返されるオブジェクトに対して #deliver! を呼んでいる。

ActionMailer::Base#mail

このメソッドはActionMailerの中核を担っているメソッドで、メール送信の行うための設定の全てを実施している。

  1. インスタンス生成時に @_messageMail::Message のインスタンスをセットしている( Mail.newMessage::Message.new を実行している)。そして attr_internal :message により message と呼び出すことで、生成したインスタンスを参照することができる。
  2. #apply_defaults にてmailメソッドに渡されたheaderにMyMailerで設定したデフォルト(今回はfromの値)がセットされる。そして、#assign_headers_to_message で一部を除いたheaderをMail::Messageのインスタンスにセットしている。
  3. メソッド名からはわかりづらいが、@_messageMail::Message インスタンスのdelivery_methodをセットしている。セットされるオブジェクトは、MyOwnDeliveryMethodClass.new({}) で生成したインスタンス。
  4. Bodyの設定。今回は文字列を直にセットしているが、通常はActionViewを使い、テンプレートレンダリングした文字列が @_message のbodyにセットされる。

3のdelivery_methodをセットする部分のコードを以下に記載しておく。

これによりヘッダー、本文、delivery_methodがセットされたMail::Messageインスタンスができあがった。
最後にこのインスタンスの #deliver! を実行することでメール送信を行なっている。

Mail::Message#deliver!

  1. 自身を引数にdelivery_methodの #deliver! を呼ぶ。これは MyOwnDeliveryMethodClass#deliver! なので、ここでメール送信処理を実装することで、オリジナルのメール送信が可能となる。
  2. ここで MyOwnDeliveryMethodClass#settings が呼ばれるので、このメソッドの実装も必要。

細かいところは省いたが、自作のメール送信方法を使ったActionMailerのメール送信処理の流れを確認することができた。

メール送信方法の自作に関して、実装するメソッドは少ない(実質 #deliver! だけ)ので簡単にできると思う。
ただし、引数で渡される Mail::Message にどんな値がセットされているかを調べるのが少し時間がかかった。
自作中は、開発環境でも config.action_mailer.raise_delivery_errors はtrueにセットしておいた方がよい。内部でエラーが発生しているのがわからず、デバッグに苦戦したため。