ActionMailerは何をしているのか

# テーマ

初Adventです！

最近､メール送信時のトラブルを分析していたのですが､ActionMailerの実装についての情報がなかなか見つからず困りました｡

この機会に、ActionMailerの実装を掘り下げ、「メールを送信したら内容がSlackに届く」というSlackMailerのサンプルに挑戦したいと思います。

## なぞのおじさん

あらためて[railsを支えるコンポーネント軍団](https://github.com/rails/rails)を眺めてみると､それはそれはツヨそうなメンバーが揃っています。

ActionPackやActiveRecordといったフレームワークを動かす最重要幹部たち､ActiveSupportという有能な参謀。さらにはActionCableやActiveStorageのような期待の大型新人もいます｡

よく見るとわれらがActionMailerも｢地味な窓際おじさん｣といった風情で鎮座しておられますね｡

いまひとつ注目されないこの古株おじさんを､愛と尊敬を持って観察してみましょう。

## 動きを観察する

このActionMailerおじさん､実はかなりミステリアスな技を持っています｡

さっそく適当にSampleMailerクラスを作ってみましょう｡

```ruby
# app/mailers/sample_mailer

class SampleMailer < ActionMailer::Base

  def hello(user) # emailを持つUserモデルを用意しておく
    @user = user
    mail(from: 'xxx@example.com' ,to: user.email, subject: "Hello")
  end
end
```

[ここ](https://railsguides.jp/action_mailer_basics.html#%E3%83%A1%E3%82%A4%E3%83%A9%E3%83%BC%E7%94%9F%E6%88%90%E3%81%AE%E5%85%A8%E6%89%8B%E9%A0%86)にあるように､ActionMailer::Baseを継承したSampleMailerクラスに`#hello`メソッドを書くだけ｡簡単ですね｡

対応するビューもつくっておきます｡

```ruby
# app/views/sample_mailer/hello.text.erb

<%= "Hello #{@user.nickname}" %>
This is a test mail.
```

すると、簡単にメールを作成できます。

```ruby
pry(main)> user = User.first # 適当なUserオブジェクトを入れておく
pry(main)> mail = SampleMailer.hello(user)
[INFO ]   Rendering sample_mailer/hello.text.erb
[INFO ]   Rendered sample_mailer/hello.text.erb (1.8ms)
=> #<Mail::Message:70193536029860, Multipart: false, Headers: <From: xxxxx@example.com>, <To: yyyyy@example.com>, <Subject: Hello>, <Mime-Version: 1.0>, <Content-Type: text/plain>>
```

まあ、ここまでは普通です・・よね。

いやいや。

### メソッド違い？

先ほどSampleMailerクラスに定義したのは**インスタンスメソッド**でした｡

しかし`SampleMailer.hello`で呼ばれるのは､SampleMailerの**クラスメソッド**のはずです｡

おかしいと思いませんか？

試しに､似たような使い方をするコントローラーを見てみましょうか｡

```ruby
pry(main)> UsersController.instance_methods(false) # UsersController#showを定義
=> [:show]

pry(main)> UsersController.show # エラー
NoMethodError: undefined method `show' for UsersController:Class
```

当然エラーですw
なぜメールだけはクラスメソッドで呼ぶ（呼べる）のでしょう？

### さらにもうひとつ

`hello`メソッドはActionMailer::MessageDeliveryインスタンスを産み落とします｡

```ruby
pry(main)> mail.class
=> ActionMailer::MessageDelivery
```

これはおそらく､先程書いたメソッドの最後にある`#mail`メソッドが､テンプレート`hello.text.erb`をベースに作成したメールオブジェクトを作り出しているものと思われます｡

試しに､｢まったく何もしないメソッド｣を定義してみましょうか｡

```ruby
class SampleMailer < ActionMailer::Base
  def nop
  end
end
```

呼び出してみます｡

```ruby
pry(main)> empty_mail = SampleMailer.nop
pry(main)> empty_mail.class
=> ActionMailer::MessageDelivery
```

あれ･･･さっきと同じクラスのオブジェクトだ｡
中で何もしてないのになぜ返ってくるの？

ちなみに､あえて**インスタンスメソッド**として#nopを呼んでみると

```ruby
pry(main)> empty_mail2 = SampleMailer.new.nop
pry(main)> empty_mail2
=> nil
```

期待通りに`nil`が帰ります｡

ねえ､おじさん！？
私のメールに何をしてるの？

## ActionMailerマジック

おじさんの真の姿は黒魔術に守られていて､pryの力を持ってしてもなかなか正体が見えてきません｡

というわけで、ソースコードから解読していきましょう｡

まずSampleMailerのベースクラスである[ActionMailer::Baseの実装](https://github.com/rails/rails/blob/master/actionmailer/lib/action_mailer/base.rb)を見てみます｡
我々が叩いたのは**クラスメソッド**のはず､ですよね｡

```ruby
module ActionMailer

  class Base < AbstractController::Base

    # モジュールのインクルード､クラス変数の宣言など

    class << self

      # snip

      def method_missing(method_name, *args)
        if action_methods.include?(method_name.to_s)
          MessageDelivery.new(self, method_name, *args)
        else
          super
        end
      end

      def respond_to_missing?(method, include_all = false)
        action_methods.include?(method.to_s) || super
      end
     end

   # snip
```

**おまえかーーー!!**

いきなりダークそうなやつが潜んでいました｡

見ての通り､メソッド名`method_name`が､`action_methods`内に定義されていれば､ActionMailer::MessageDeliveryをnewして返しています｡[^1]

[^1]: 定義されていなければ通常通りに継承チェーンを上がっていき､`NoMethodError`に行き着く

継承元AbstractController::Baseに定義されている`action_methods`は､SampleMailerに生えているpublicなインスタンスメソッドをまとめて`Set`に入れて戻してくれます｡ [^3] [^4]

つまり今回のケースだと､**ないはずのクラスメソッド**を呼び出した時に､`"nop"`､`"hello"`という文字列のいずれかと一致していたら､ActionMailer::MessageDeliveryインスタンスが戻ってくるという仕組みです。

```ruby
pry(main)> SampleMailer.action_methods
=> #<Set: {"nop", "hello"}>
```

`MessageDelivery.new(self, method_name, *args)`の引数部分については（例えば`SampleMailer.hello`が呼ばれたとすると）、こんな感じになるはずです。

```ruby
  # self        ...  SampleMailer
  # method_name ...  :hello
  # *args       ...  user (Userインスタンス)
```



[^3]: 正確には､レシーバSampleMailerの継承チェーン中でもっとも末端側の｢**abstractクラスであるActionMailer::Baseをのぞいた子孫側** にある全クラス｣に定義されているpublicなインスタンスメソッドの名前の集合｡つまり､このSampleMailerがHogeMailer < ActionMailer::Baseを継承している場合は､「HogeMailerのpublicインスタンスメソッドも含めた集合」が戻る｡

[^4]: AbstractControllerがどういう存在か､そもそもabstractという概念は何を意図したものなのか､というところがまた面白かったのですが､話がそれるので[こちら](https://github.com/rails/rails/blob/55d4cf2a9c1a6e77ed7aedb866e964039bb4a143/actionpack/lib/abstract_controller/base.rb#L35-L85)を参照｡

## さらなるミステリー

これで｢クラスメソッドを叩いたらインスタンスメソッドが呼ばれる｣｢何も書いていなくてもActionMailer::MessageDeliveryオブジェクトが返ってくる｣という理由が理解できました｡

しかし､なんでそんなややこしいことをしているのか？

それでは、この後の動きを詳しく追いかけてみましょう｡
先ほどの処理の最後の方で、ActionMailer::MessageDelivery.newをしていましたよね。

```ruby
module ActionMailer
  class MessageDelivery < Delegator
    def initialize(mailer_class, action, *args) #:nodoc:
      @mailer_class, @action, @args = mailer_class, action, args
      @processed_mailer = nil
      @mail_message = nil
    end
    # snip
```

なんと､ただ渡された中身をインスタンス変数に持たせるだけです｡

処理はここまで｡

・・・

・・・・？

**いやいやいやいやちょっと待って**


```ruby
pry(main)> mail = SampleMailer.hello(user)
[INFO ]   Rendering sample_mailer/hello.text.erb
[INFO ]   Rendered sample_mailer/hello.text.erb (1.8ms)
```

ログ上にはテンプレートをレンダリングしたというが形跡がしっかり残っていましたよね？

おじさん・・・まだ裏で何かやってますね。

## おじさんの闇の衣

実はここで一度行き詰まっていたのですが､ヒントはDelegatorにありました｡

```ruby
  class MessageDelivery < Delegator # おじさんがまとってる闇の衣

    # snip

    def __getobj__ #:nodoc:
      @mail_message ||= processed_mailer.message
    end
```

Delegatorとはその名の通り､rubyで委譲を行うために[用意された方法](https://ruby-doc.org/stdlib-2.1.0/libdoc/delegate/rdoc/Delegator.html)の一つ｡

ちょくちょく見かける`fowardable`とは違い､**委譲先を動的に設定できる**というすごい裏技です｡

上の例では `__get_obj__`が委譲先を指定している部分にあたります｡これのおかげでActionMailer::MessageDeliveryがnewされたタイミングで､自身の持たないメソッドは､`processed_mailer.message`で表されるオブジェクトに委譲する、という設定がされます。

この`__getobj__`が初期化のタイミングで走るため、**processed_mailer.messageが評価される** ことになります｡

(newした後に続きがあった！！)

```ruby
module ActionMailer
  class MessageDelivery < Delegator

    # snip

    private
      def processed_mailer
        @processed_mailer ||= @mailer_class.new.tap do |mailer|
          mailer.process @action, *@args
        end
      end

      # snip
```

先ほどの例で言うと､SampleMailerをnewした上で､そのインスタンスに対して`process(user)`を叩きます。

ここで親クラスActionMailer::Baseの`#initialize`と`#process`が順次よばれます。

```ruby
    def initialize
      super()
      @_mail_was_called = false
      @_message = Mail.new
    end

    def process(method_name, *args)
      payload = {
        mailer: self.class.name,
        action: method_name,
        args: args
      }

      ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
        super
        @_message = NullMail.new unless @_mail_was_called
      end
    end
```

newの方は単純で､Mail::Messageインスタンス[^5]を作成して@_messageという内部変数[^6]に持たせます｡この[Mail](https://github.com/mikel/mail)とは､rubyのメールライブラリで､これが配信作業を支えています。

[^5]: 一見Mailインスタンスに見えるが、Mail.newは最終的に[Mail::Message.newを呼ぶため]、Mail::Messageインスタンスが戻る。（https://github.com/mikel/mail/blob/61f0f01deaaae4bddb40932179bb5e18ce518f76/lib/mail/mail.rb#L50）
[^6]: 実態はattr_internalなので､アクセサ名はmessage､変数名は@_messageという形になっている｡

つまりActionMailerおじさんは直接送信をさばいているわけではなく､実務をMailに丸投げしています。

一方`process`はsuperのところで、[AbstractControllerに定義された#process](https://github.com/rails/rails/blob/55d4cf2a9c1a6e77ed7aedb866e964039bb4a143/actionpack/lib/abstract_controller/base.rb#L125-L135)を呼び出し､それは最終的に`#process_action`に渡されます｡

```ruby
module AbstractController
  class Base
     #snip

     private

     def process_action(method_name, *args)
        send_action(method_name, *args)
     end

     alias send_action send
```

要は`send`です｡レシーバselfは`SampleMailer`でしたね。

ということは、ここで初めて､**インスタンスメソッドSampleMailer#hello(user)が実行された**ことになります!!

ついに･･ついに見つけたどー ＼(^o^)／

## メールはどこでレンダーされているのか

処理の流れがつかめてめでたしめでたし････

しかし、まだテンプレート`app/views/sample_mailer/hello.text.erb`はいったいどこでレンダーされているのか､という疑問が解決していません｡

結論から言うと、最初にMailerのアクション内に書いていた`mail`メソッドが､ここで初めて活躍するのです｡(直前で`SampleMailer#hello`がsendされたので､そのままよばれますね)

実装は､[ActionMailer::Base#mailの中](https://github.com/rails/rails/blob/55d4cf2a9c1a6e77ed7aedb866e964039bb4a143/actionmailer/lib/action_mailer/base.rb#L811-L843)にあります｡


```ruby
    def mail(headers = {}, &block)
      return message if @_mail_was_called && headers.blank? && !block
      # （中略）　headerの中身を作成

      wrap_delivery_behavior!(headers[:delivery_method], headers[:delivery_method_options])

      assign_headers_to_message(message, headers)
      # （中略）　メッセージを取り出す（レンダリングはココ！！）
      @_mail_was_called = true
      # （中略）　multipartメールの組み立てなど
      message
    end
```

・・・ついに、ついに発見しました！

ここで先ほど作成したMail::Messageインスタンスに様々な要素を突っ込むことで、メールを作成しています｡[^8]

[^8]: `mail`メソッドを全く使わない#nopのようなメソッドだと､当然レンダーはされないのですが､代わりに[NullMailインスタンス](https://github.com/rails/rails/blob/55d4cf2a9c1a6e77ed7aedb866e964039bb4a143/actionmailer/lib/action_mailer/base.rb#L612-L623)が@_massageに入ります｡ここで、`#mail`が呼ばれたかどうかを判定しているのが@_mail_was_calledフラグにあたるようです。

## メール送信はどうなっているのか

さて、ここでもう一つ。

このメソッドはメールをレンダーするだけではなく、`wrap_delivery_behavior!`というメソッドによって、メールに配送手段を与えます｡

Production環境以外をletter openerにしたり、ステージング用のメーラーを設定することがありますが、この仕組みによって最終的な配送手段を切り替えることができています。

`ActionMailer::DeliveryMethods`という`Concern`モジュールをみてみると・・

```ruby
module ActionMailer
  module DeliveryMethods
    extend ActiveSupport::Concern

    included do
      # snip

      def wrap_delivery_behavior(mail, method = nil, options = nil)
        method ||= delivery_method
        mail.delivery_handler = self

        case method
        # snip

        when Symbol # ここで配信メソッドを与えている
          if klass = delivery_methods[method]
            mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))

        # snip
        end

        mail.perform_deliveries    = perform_deliveries
        mail.raise_delivery_errors = raise_delivery_errors
      end
    end

    def wrap_delivery_behavior!(*args) # このまま上のクラスメソッドを呼び出す
      self.class.wrap_delivery_behavior(message, *args)
    end
  end
```

どうやら前半のcase文で行われていることが核心のようです。

つまり、実際に配信を担当する機能を持ったクラスを取り出して`Mail#delivery_method`に渡す。（ActionMailerのデフォルトでは`Mail::SMTP`クラスが[渡されています](https://github.com/rails/rails/blob/1b24fb213b820ebca29eb1d38efb07a410c7afaa/actionmailer/lib/action_mailer/delivery_methods.rb#L18-L27)。）この時、オプション類も一緒に渡します。

Mail::Messageの実装を追いかけると、

```ruby
module Mail
  class Message
    def delivery_method(method = nil, settings = {})
      unless method
        @delivery_method
      else
        @delivery_method = Configuration.instance.lookup_delivery_method(method).new(settings)
      end
    end
```

ここで`Configuration.instance.lookup_delivery_method`というシングルトンメソッドは、デフォルト設定された配信手段以外は、[与えられたクラスをそのまま戻します](https://github.com/mikel/mail/blob/61f0f01deaaae4bddb40932179bb5e18ce518f76/lib/mail/configuration.rb#L29-L50)。

つまりMail::Messageオブジェクトは、渡されたクラスを（一緒に渡されたオプションを使って）初期化し、作成されたオブジェクトを@develiry_methodに格納します。

なお、この配送設定を行うにはいくつか方法がありますが[^12]、一般的にはconfig/environment/以下の設定ファイル内で

```ruby
  config.action_mailer.delivery_method = :hogemail # このシンボルに対応するクラスがnewされる
  config.action_mailer.hogemail_settings = { # newする際に渡されるオプション
    domain:  'oreore.jp'
    something: :something
  }
```

のように行っているのがそれにあたります[^11]

そう、いつも何気なく設定していたアレは、ここのためにあったのですね。

[^11]: この場合、シンボル:hogemailに対応する配信用のクラスを設定する必要があるが、そのために[ActionMailer::Base.add_develiry_methodが利用できる。](https://github.com/rails/rails/blob/1b24fb213b820ebca29eb1d38efb07a410c7afaa/actionmailer/lib/action_mailer/delivery_methods.rb#L50-L54)

[^12]: mailメソッドに、`delivery_method`オプションを持たせることで`method`を指定することも可能。つまり動的に配信メーラーを切り替えることもできるようになっています。


## 実際の配信

さて、最終的な配信ですが、これは先ほどnewされた、配信手段を提供するオブジェクト@delivery_methodが持っている`#deliver!`というメソッドが呼ばれます。

逆に言えば、`#deliver!`というインスタンスメソッドを持つクラスであれば、どんなものでも配信を請け負うことが可能なんですね。[^10]

[^10]: 例えば有名なletter_openerは[自身が持つ#develer!メソッドの中で](https://github.com/ryanb/letter_opener/blob/befbe9812860ac29760a3171273569d6dca8101e/lib/letter_opener/delivery_method.rb#L26-L32)送信メールオブジェクトを受け取って表示しています。

ついでに言うと、このdeliverの前後にはinterceptorやobserverといった[コールバックを設定することが可能です](http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails)。これを利用して例えば、ステージング環境からのメールを特定の宛先に変更してしまう、なんていう使い方もできるのですね。

ともあれ、ようやくメール配信という仕事の全容が見えました。

## まとめ

かなり話が複雑なので、ここまでの流れをまとめてみたいと思います。

#### 1. クラスメソッドSampleMailer.hello(user)が叩かれた時

<img width="675" alt="スクリーンショット 2017-12-15 10.01.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/151343/f4f68c4b-1be3-4c81-0e18-a2912e816e86.png">

* ActionMailer::BaseがActionMailer::Deliveryインスタンスを作成して返す

* ActionMailer::DeliveryインスタンスはSampleMailerインスタンスを作成し、`SampleMailer#process(:hello, user)`を実行

* ActionMailer::Deliveryインスタンスは、SampleMailerが内部で作成するMail::Messageを委譲先に設定

#### 2. SampleMailer#process(:hello, user)で起こること

<img width="673" alt="スクリーンショット 2017-12-15 10.01.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/151343/a244a2ee-2fd6-8063-a209-42048e912286.png">

* SampleMailer#hello(user)が実行される。`mail`メソッドが使われている場合、`ActionMailer::Base#mail`が呼ばれる。

* `mail`メソッドは、ActionMailer::Baseが持つ様々なメソッドを利用する。

* まず適切なテンプレートを探してレンダーし、その内容を、引数にもたせた値やconfigと一緒に、Mail::Messageオブジェクトに設定。

* つぎにMail::Messageに対して、実際の配信を担うdelivery agentクラスを設定する。このクラスはすぐに`new`され、実際の配信時に`#deliver!`メソッドが叩かれる。

#### 3. メール送信を行う

<img width="673" alt="スクリーンショット 2017-12-15 10.01.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/151343/5475038b-6774-4106-1c68-0e4a75d83c80.png">

* `ActionMailer#deliver_now(later)`は、Mail::Messageの`#deliver`メソッドを呼ぶ

* Mail::Messageは、(２)で設定されたdelivery agentオブジェクトに対して、`#deliver!`メソッドを実行する。delivery agentにMail::Messageオブジェクトが渡される。

* agentがMail::Messageオブジェクトを実際に発送する

とまあこんな感じでしょうか。

それにしてもややこしい😮

`ActionMailer`は自分で配送作業しているわけではなく、要件ごとに発注先を組み合わせて、複雑な配送業務をそつなく回すデキるおじさんだったのですね。


### 遊んでみる

ここまでわかったmailerの仕組みをつかえば、たとえばメールの内容を送信する代わりにslackに流すお手製エージェントが作れそうです。

というわけで、**本番環境ではない場合に限り、全ての送信メールを横取りして、代わりにslackの指定チャネルに送信**してくれる機能のデモをつくってみした。

![slack_mailer.gif](https://qiita-image-store.s3.amazonaws.com/0/151343/73b13d66-d2aa-b023-3f39-d7f642ee2643.gif)

仮に「会員登録の際にお知らせメールを自動配信する雑アプリ」をつくって、その中で動かしています。

* メールオブジェクトから内容を抜き出してスラックの指定チャネルに投げる、というSlackMailerエージェントを作成。

* 次に`#mail`メソッドを`ApplicationMailer`クラスでいったん上書きします。そこで条件に応じた適切な`delivery_method`オプションをヘッダーに指定した上で、superします。（以降は本来の動作になる）

* `ApplicationMailer`を継承する全てのMailerでこの機能が使えます。もちろん`ActionMailer::Base`から直接継承したMailerをつくると、普通通りメールを出すことも可能。

デモ機能は[こちら](https://github.com/fursich/slack_mailer)に置きました。

記事が長くなってしまったので、またニーズがあれば解説記事など書いてみたいと思います。


### 環境

環境は以下の通りです。

* ruby 2.3.1

* rails 5.1.4

* railsリポジトリは２０１７/１２月時点（一部を抜粋、コメントなど割愛しています）

エンジニア歴１年に満たない初心者の読み解きなので、おかしな点があればビシバシとご指摘いただければありがたいです！

### あとがき

#### やろうと思ったきっかけ

Webサービス開発現場で､ユーザーにメールを配信するため､[mailgun](https://www.mailgun.com/)というAPIベースのメールサービスを利用しています｡

ラッパーとして最近アップデート状況がよくなっているmailgun-ruby（公式gem）を試用していたところ､テキストメールがコケるという謎の現象に見舞われました｡[^20]

しかたなく修正PRをたてたのですが[^22]､状況を再現するテストケースを立てる際に「そもそもメール送信がどういう実装になっているのかがわからなすぎてテストがかけない」という困った状況になったことをきっかけに調べ始めました。

マニアックな知識ですが、どこかの誰かの役に立てば幸いです^^


[^20]: [テキストメール(またはhtmlメール）を､おかしな方法でmultipart化してしまっている](https://github.com/mailgun/mailgun-ruby/issues/130)ことが判明｡

[^22]: 修正が取り入れられたので、次のバージョン1.1.9で直る予定