📧
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を見ていく。
※ なぜ上記の対応でメールテンプレートが使い分けられるのかのみに注目するため、各実装を細かくは解説しない。
-
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
-
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
-
Devise::Models::Authenticatable#send_devise_notification
devise_mailer.send(notification, self, *args)を読みやすくすると、Devise::Mailer.send(:reset_password_instructions, <モデルのインスタンス>, *args)になる。selfでモデルのインスタンスが呼ばれる理由は、モデルクラスにdevise :database_authenticatableでDevise::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"
-
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
-
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
-
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
-
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