📧

devise auth tokenでモデルごとに配信するメールテンプレートを切り替える方法

に公開

devise、devise token authの実装を見てそうなってるのかーと思った + 120割の確率で忘れるのでメモ。
前からdeviseを使ってる方からしたら「こいつ今更なに言ってんだ?」だと思うけど許して😢

結論

views/<TBL名>/mailer配下にテンプレートを配置する。
共通して使用したいテンプレートはviews/devise/mailerに配置する。

my_project
  └ app
     ├ models
     │  ├ student.rb
     │  └ teacher.rb

     └ views
        ├ students
        │  └ mailer
        │     └ reset_password_instructions.html.erb

        │ teachers
        │  └ mailer
        │     └ reset_password_instructions.html.erb

        └ devise
           └ mailer
              └ confirmation_instructions.html.erb

実装を見る

DeviseTokenAuth::PasswordsController#createを見ていく。
※ なぜ上記の対応でメールテンプレートが使い分けられるのかのみに注目するため、各実装を細かくは解説しない。

  1. DeviseTokenAuth::PasswordsController#create
    中央の@resource.send_reset_password_instructionsがメールを構築して送信しているメソッド。@resourceにはDeviseTokenAuth::Concerns::Userをincludeしているモデルのインスタンスが入る。
def create
  return render_create_error_missing_email unless resource_params[:email]

  @email = get_case_insensitive_field_from_resource_params(:email)
  @resource = find_resource(:uid, @email)

  if @resource
    yield @resource if block_given?
    @resource.send_reset_password_instructions( # <== こいつ
      email: @email,
      provider: 'email',
      redirect_url: @redirect_url,
      client_config: params[:config_name]
    )

    if @resource.errors.empty?
      return render_create_success
    else
      render_create_error @resource.errors
    end
  else
    render_not_found_error
  end
end
  1. DeviseTokenAuth::Concerns::User#send_reset_password_instructions
    send_devise_notification:reset_passowrd_instructionsと生成されたトークン + send_reset_password_instructionsの引数を渡され呼び出される。
def send_reset_password_instructions(opts = {})
  token = set_reset_password_token

  # fall back to "default" config name
  opts[:client_config] ||= 'default'

  send_devise_notification(:reset_password_instructions, token, opts) # <== こいつ
  token
end
  1. Devise::Models::Authenticatable#send_devise_notification
    devise_mailer.send(notification, self, *args)を読みやすくすると、Devise::Mailer.send(:reset_password_instructions, <モデルのインスタンス>, *args)になる。selfでモデルのインスタンスが呼ばれる理由は、モデルクラスにdevise :database_authenticatableDevise::Models::Authenticatableをincludeしているため。
    こいつがメールメッセージを作ってくれる。その下のmessage.deliver_nowはActionMailerのメソッドで、名前の通りメールを送ってる。
def send_devise_notification(notification, *args)
  message = devise_mailer.send(notification, self, *args)
  message.deliver_now
end

protected

def devise_mailer
  Devise.mailer
end
# lib/devise.rb
# Devise::Getter
def self.mailer
  @@mailer_ref.get
end

def self.mailer=(class_name)
  @@mailer_ref = ref(class_name)
end
self.mailer = "Devise::Mailer"
  1. Devise::Mailer#reset_password_instructions
    Devise::Mailers::Helpers#devise_mailが呼ばれる。
def reset_password_instructions(record, token, opts = {})
  @token = token
  devise_mail(record, :reset_password_instructions, opts)
end
  1. Devise::Mailers::Helpers#devise_mail
    やっと本題の話ができるとこまで来た...。
    注目すべきはdevise_mailで呼ばれているheaders_forと、さらにその中で呼ばれているtemplate_paths。順に説明します。
    ちなみにmailはActionMailerのメール作成メソッド。
def devise_mail(record, action, opts = {}, &block)
  initialize_from_record(record)
  mail headers_for(action, opts), &block
end
  1. Devise::Mailers::Helpers#headers_for
    mailメソッドがメールを生成するために必要なパラメータheadersを生成している。
    mailはtemplate_path + template_nameを走査して同じパス、同じ名前のhtml.erbをメールテンプレートとして採用する。
    template_nameには:reset_password_instructionsが入る。
def headers_for(action, opts)
  headers = {
    subject: subject_for(action),
    to: resource.email,
    from: mailer_sender(devise_mapping),
    reply_to: mailer_sender(devise_mapping),
    template_path: template_paths,
    template_name: action
  }
  # Give priority to the mailer's default if they exists.
  headers.delete(:from) if default_params[:from]
  headers.delete(:reply_to) if default_params[:reply_to]

  headers.merge!(opts)

  @email = headers[:to]
  headers
end
  1. Devise::Mailers::Helpers#template_paths
    こいつが一番大事。というかこいつを説明するためだけにここまで書いた。
    _prefixesメソッドは、こいつが呼ばれた名前空間のprefixのみいい感じにpathに変換して返すdeviseのメソッド。ここだとdevise/mailerが返されてtemplate_pathに代入される。
    @devise_mapping.scoped_pathは、メール配信対象のモデルのパスを返す。Teacherモデルの場合はteachers。それに/mailerを引っ付けてtemplate_path.unshiftしているので、最終的なtemplate_pathは["teachers/mailer", "devise/mailer"]
def template_paths
  template_path = _prefixes.dup
  template_path.unshift "#{@devise_mapping.scoped_path}/mailer" if self.class.scoped_views?
  template_path
end

devise_mailに戻って、
先述した通り、template_path + template_nameを走査して一致するメールテンプレートを採用するので、teachers/mailer/reset_password_instructions.html.erbを走査し、無ければdevise/mailer/reset_password_instructions.html.erbを採用という流れになる。

def devise_mail(record, action, opts = {}, &block)
  initialize_from_record(record)
  mail headers_for(action, opts), &block
end

まとめ

たかだかどのメールテンプレートを使用するかだけでどんだけ深くまで掘り下げなきゃいけないんだと思った。
とはいえこれでまた少しdeviseに詳しくなれた気になりました。

Discussion