Railsの技: Action Mailerを活用する良い書き方を改めておさらいする(翻訳)
メーラーは、文字通りあらゆるRailsアプリケーションで使われる機能です。しかしメーラーは後から導入されることが多く、Railsアプリケーションを正しく書く鉄則が放ったらかしにされがちです。
メーラーを書くという作業は、コードベースで「一度設定したら後は忘れる」部分になります。しかし最近自分のアプリケーションのメーラーを見返したときに、2つの点でショックを受けました。自分が昔書いたメーラーのひどさと、そして自分の知らない素晴らしいメーラー機能がRailsに豊富に揃っているという事実です。
私はもう10年以上Railsアプリケーションを書き続けていますが、メーラーについて今週私が得た学びがいくつもあったので、今後新しいデフォルトとして使っていくつもりです。
お知らせ
ソフトウェアについて考えるのが好きな方、当"Boring Rails"スタイルでコードを書くのが好きな方へ: Arrowsではプロダクトエンジニアを募集しています。私と一緒に働いてみませんか?
🔗 メールの表示名は専用ヘルパーでやろう
Railsには、メールクライアントで表示される表示名(display name)をフォーマットする組み込みヘルパーがあります。
# 表示名なし
ActionMailer::Base.email_address_with_name("help@arrows.to", "Arrows HQ")
=> "Arrows HQ <help@arrows.to>"
# 表示名あり
ActionMailer::Base.email_address_with_name(user.email, user.display_name)
=> "Matt Swanson <matt@boringrails.com>"
大した機能には見えないかもしれませんが、このdisplay_name
メソッドはnil
値も扱えますし、引用符もエスケープしてくれます。アプリから配信するメールを洗練させるのにとても良い小技です。
このヘルパーについては過去記事でも書いたことがありますが、この小技を紹介するたびに「これ知らなかった!」というお便りをくださる方が何人もいるので、ここでも紹介しておきたいと思います。
🔗 メーラーのビューフォルダを変更する
メーラーで使うビューのファイル構造はいつも悩みのタネのひとつでした。デフォルトでは、たとえばNotificationMailer.welcome_email
はapp/views/notification_mailer/welcome_email.html.erb
ファイルに置かれます。
このファイル構造には、Railsのコントローラが動作するときの仕組みが反映されています。しかしこのままでは、すべてのメールテンプレートを一度に見ようとするときに面倒です。メールの表示方法を変更するたびに、変更に問題がなかったかどうかを確かめるために、あちこちにあるメールのテンプレートを片っ端からチェックしなければなりませんでした。
Andy Croll氏による以下の記事で、メーラーのビューテンプレートを1箇所にまとめる方法があることを知りました。ApplicationMailer
に少し手を加えるだけで、メールのテンプレートを探しやすいフォルダ構造になります。
参考: All Your Mailer Views in One Place - Andy Croll
class ApplicationMailer < ActionMailer::Base
prepend_view_path "app/views/mailers"
end
これで、メーラーのビューをapp/views/mailers/notification_mailer/welcome_email.html.erb
に置けるようになりました。フォルダのネストが1階層増えはしますが、app/views/mailers
フォルダに置くようにしたことで、メーラーのビューとコントローラのビューを同じapp/views/フォルダに置かずに済みます。
🔗 1個のメーラーで複数のメール送信を扱う
1個のメーラーで複数のメール送信を扱えることをご存知でしたか?
Railsのドキュメントには、1個のメーラーで複数のメール送信を扱ってはいけないとは書かれていませんが、明示的に推奨されているわけでもありません(インターネット上では不特定の人々から明示的に許諾を得なければならなくなることもあったりしますが、それを思えば幸いです)。
私がこれまで手がけてきたどのRailsアプリも、何らかの理由でMailer
クラスとメール送信メソッドが1対1対応していました。これでは、システムで扱っているメール送信を把握するのが面倒になるだけでなく、命名に奇妙な軋轢が生じてコードの臭いも漂ってきます。
以前の私は、以下のようなメーラーを書いていました。
class CommentReplyMailer < ApplicationMailer
layout "minimal"
def comment_reply_email(user, comment)
# mail(to: ...)
end
end
class UserMentionedMailer < ApplicationMailer
layout "minimal"
def mentioned_email(mentionee, comment)
# mail(to: ...)
end
end
当時そのようにした理由は自分でもよくわかりませんが、分割する理由を正当化できません。
代わりに、以下のようにメール送信機能を1個のメーラーでグループ化すればよいのです。
class NotificationMailer < ApplicationMailer
layout "minimal"
def comment_reply(user, comment)
# mail(to: ...)
end
def mentioned(mentionee, comment)
# mail(to: ...)
end
end
これで、1個のファイルですべてのメール通知を見渡せるようになりました。配信されるメールがあることを見落として、アプリに新機能を追加したときに更新がユーザーに通知されなかった、といったことが起きがちですよね。
🔗 命名に"email"という語は不要
メーラーはメールを送信するものなので、メソッド名やビュー名の末尾に_email
を追加する必要はありません。前述の小技を用いてビューをapp/views/mailers/
フォルダに置く場合は特にそうです。
# このコードがメールを送信することはわかりきっている
NotificationMailer.comment_reply_email(@user, @comment).deliver_later
# なので、名前の末尾に_mailは不要!
NotificationMailer.comment_reply(@user, @comment).deliver_later
🔗 "パラメータ化"メーラー
さて、ここまで紹介した小技はどれも気が利いていますが、驚くほどの技ではありませんでした。
最近の私は、メールを自分たちの送信ドメインからトランザクショナルに送信できる機能を構築中です。この機能では、ユーザーが受け取るシステムメールの送信元をhello@arrows.to
ではなくonboarding@acme-saas.com
に変えます。舞台裏ではDNS関連の動作も関わってきますが、今はこの機能のメーラー部分に注目したいと思います。
ここで難しいのは、あるアカウントがカスタム送信ドメインをセットアップしている場合、メーラーでFrom
アドレスが確実に上書きされるようにすることです。アプリケーションには多数のメーラーがあるのですが、この条件コードをいちいちメーラーごとに追加したくありません。こんな調子では、いつかそのうち新しいメール送信を追加するときに、ユーザーがカスタム送信ドメインを使っていることを忘れるのではないかと心配でした。
手始めに、中身がぎっしり詰まっているメーラーの1つで基本的な実装を行ってみました。
class NotificationMailer < ApplicationMailer
def comment_reply(user, comment)
# ...
mail(
to: user.email,
from: build_from_address(comment.account)
)
end
private
def build_from_address(account)
if account.custom_email_sender?
email_address_with_name(
account.custom_email_address,
account.custom_email_name
)
else
email_address_with_name("hello@arrows.to", account.name)
end
end
end
最初に思いついたのは、 Current.account
をチェックするコードを少しばかりApplicationMailer
に追加するという方法でしたが、この方法は行き詰まってしまいました。このメーラーは( deliver_later
経由で)バックグラウンドジョブから送信すべきなので、カレントリクエスト(そしてCurrent
属性)のコンテキストが失われてしまうのです。
回避方法はいろいろ考えられるのですが(Current
属性をジョブに渡すミドルウェアなど)、どうもしっくり来ません。
もっといい方法でこれを実装できないかと思って探し回るうちに、RailsガイドのAction Mailerドキュメントに立ち戻り、そこで1つ発見がありました。どのサンプルコードも、データをメーラーに直接渡していないのです。その代わり、あらゆるものにparams
経由でアクセスしています。
NotificationMailer
.comment_reply(user, comment)
.deliver_later
つまり、上のように書くのではなく、with
で以下のように書くということです。
NotificationMailer
.with(user: user, comment: comment)
.comment_reply
.deliver_later
このパターンはまったく使ったことがありませんでした。最初にRailsを学んで以来(2.xの時代です!)、私のメーラーの書き方は何ひとつ変わっていなかったのですが、実はRails 5.1から"パラメータ化"メーラーという概念が導入されているのです。
第一印象では、どの点が嬉しいのかが今ひとつ理解できませんでした。私が一般的に好んでいるのは、一般的なparams
ハッシュよりも、メーラーメソッドに明示的にメソッド引数を渡す方法です。
しかし今回は、最終的に「なるほど!」と膝を打ちました。
with
とパラメータ化メーラーを使えば、メーラーにbefore_action
コールバックを追加することで、カスタム送信ドメインのようなオプションをメーラーメソッドのコンテキストの外で設定できるというメリットも得られるのです。
このコンセプトに沿って以下のように少し手を加えてみました。
class NotificationMailer < ApplicationMailer
before_action { @account = params[:account] }
before_action { @from = build_from_address }
def comment_reply(user, comment)
# ...
mail(to: user.email, subject: "New reply", from: @from)
end
def mentioned(mentionee, comment)
# ...
mail(to: mentionee.email, subject: "You were mentioned", from: @from)
end
private
def build_from_address
if @account.custom_email_sender?
email_address_with_name(
@account.custom_email_address,
@account.custom_email_name
)
else
email_address_with_name("hello@arrows.to", @account.name)
end
end
end
この時点ではまだ改良の余地はあるものの、各メーラーからこの設定を切り出せるようにする準備が整いました。
🔗 動的なdefault
オプション
次なる小技は、メーラーのdefault
オプションの活用方法です。皆さんはおそらく、このdefault
オプションにlambdaを渡して値を動的に設定できるという技をご存知ないでしょう(私も知りませんでした...)。
class NotificationMailer < ApplicationMailer
# 固定値を渡すことはもちろん可能
default from: "hello@arrows.to"
# しかし...動的な値を渡せばもっと便利になる
default from: -> { build_default_from_address }
private
def build_default_from_address
# ここでメールアドレスからデフォルト値を構成する
end
end
default
の嬉しい点は、必要に応じて個別のメーラーでfrom
オプションオーバーライドできる点です。しかもデフォルト値を設定しておけば、完全に省略できます。
例のカスタム送信ドメイン機能の私の実装では、この動的なデフォルト値とパラメータ化メーラーを合わせ技にしてデータを渡すのが重要な突破口になりました。
🔗 点と点をつなぎあわせる1
動的なデフォルト値と before_action
コールバックを組み合わせることで、以下のようにデフォルト値を設定する際に params
ハッシュを利用できるようになります。
class NotificationMailer < ApplicationMailer
default from: -> { build_default_from_address }
before_action { @account = params[:account] }
def comment_reply(user, comment)
# ...
mail(to: user.email, subject: "New reply")
end
def mentioned(mentionee, comment)
# ...
mail(to: mentionee.email, subject: "You were mentioned")
end
private
def build_default_from_address
if @account.custom_email_sender?
email_address_with_name(
@account.custom_email_address,
@account.custom_email_name
)
else
email_address_with_name("hello@arrows.to", @account.name)
end
end
end
NotificationMailer.with(account: @account).comment_reply(@user, @comment).deliver_later
例のカスタム送信アドレスのロジックが、メーラーのメソッドから完全に消えました。おかげで、この振る舞いがNotificationMailer
に縛られないことも明確になりました。このコードはコールバックとパラメータ化オプションに移動したので、メーラーの新しいベースクラスで使えるようになります。
これによって、"Account
を対象とするメール送信"(特定のAccount
をコンテキストとして送信されるメール)という概念の導入にも成功しました。継承先で独自の設定や機能を追加することも可能です。
class AccountMailer < ApplicationMailer
layout "minimal"
default from: -> { build_default_from_address }
before_action { @account = params.fetch(:account) }
private
def build_default_from_address
if @account.custom_email_sender?
email_address_with_name(
@account.custom_email_address,
@account.custom_email_name
)
else
email_address_with_name("hello@arrows.to", @account.name)
end
end
end
class NotificationMailer < AccountMailer
# ...
end
class DigestMailer < AccountMailer
# ...
end
class ParticipationMailer < AccountMailer
# ...
end
さらに、今後メーラーを新たに作るときにwith(account: @account)
設定をうっかり省略してしまうこともなくなるので、ささやかながら開発エクスペリエンスが向上しました。
class AccountMailer < ApplicationMailer
# ...
before_action { @account = params.fetch(:account) }
end
params
が省略されるとbefore_action
がNoMethodError
で失敗します。
呼び出し側がaccount
を渡し忘れると、それを利用しているfetch
がKeyError: :account
で失敗します。
🔗 最後に
Railsアプリケーションで書くメーラーは、品質面において最も質が落ちる部分です。Railsフレームワークのメール送信には極めて強力かつエレガントな概念が凝縮されているにもかかわらず、アプリケーションコードにあるメーラーは一度書いたらそれっきり放置されがちです。
メーラーがシステムの隅っこに位置しているせいで、メーラーのコードが複雑になったり重複しまくったりしていても見落とされがちです。メールのHTMLビューのレンダリングが難しいこともあって、「一度書いたらそれっきり」という悪い習慣がさらに後押しされてしまいます。
しかし、コードベースの他の部分と同様に、メーラーにもコードを整理整頓するためのツールがちゃんとあるのです。
私のメーラーは自分が最初にRailsを学んで以来ホコリをかぶったままでしたが、ささやかな変更をいくつか加えるだけでコードがこれほど目覚ましく改善されたことに驚きました。
特に私の場合、横断的な振る舞い(送信者アドレスのカスタマイズ)をベースのメーラークラスで抽象化することに成功しました。動的なデフォルト値とパラメータ化メーラーを合わせて使うことで、今後書かれるコードで"正しいこと"を楽に実現できるという快適な開発エクスペリエンスを提供できるようになったのです。
ひと手間かけて命名やフォルダ構造にも工夫を加えたおかげで、メーラーのメソッドが読みやすくなり、アプリのメールビューの全体像もわかりやすくなりました。いくつかのメール送信をグループ化して1個のメーラークラスにしたことで、処理内容の近いコードを凝縮した形でまとめておけるようになりました。
今後は、アプリケーション固有のメーラーコンテキストの定義を増やしていくつもりです。たとえば、AccountMailer
ベースクラスはアカウントを対象にメールを生成し、SystemMailer
ベースクラスは、ログインやパスワードリセットのメールをさまざまな設定オプションで送信する、といった具合です。
皆さんのメーラーも、少し手を加えてあげるだけでコードベースの模範になるでしょう!
本記事が役に立つと思った方は、ぜひフォームでニュースレターの登録をお願いします。余分な文章を削ぎ落とした読みやすく価値ある情報だけをニュースレターで配信いたします。
関連記事
- 訳注: "Connecting the dots"はスティーブ・ジョブスの有名な言葉です。参考: Steve Jobs - Connecting The Dots - Motivational Video - YouTube ↩
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。