Action Mailerのpreview機能を使って、Railsアプリケーションから送るメールを一覧するページを作った
ある時「アプリケーションがどういうタイミングでどういうメールを送るか、エンジニア以外も把握したい」という要望が社内で上がりました。 これはもっともな要望で、アプリケーションがどういうメールを送っているのか分からずユーザーサポートするのはしんどいことが容易に想像できます。 ところが、今まではどういうメールが送られるかを調べるにはコードを読むしかありませんでした。
この問題を解決するためにAction Mailerのpreview機能を使ったので、紹介します。
なおRailsのバージョンはv5.2.4.2を対象としています。
Action Mailerのpreview機能とは
Action Mailerのpreview機能の情報はあまり多くありません。
とはいえRails Guideに少しだけ記述があるので、まずはそれを見てみましょう。
Action Mailerのプレビュー機能は、レンダリング用のURLを開くことでメールの外観を確認する方法を提供します。上の例の
UserMailer
クラスは、プレビューではUserMailerPreview
という名前にしてtest/mailers/previews/user_mailer_preview.rb
に配置すべきです。welcome_email
のプレビューを表示するには、同じ名前のメソッドを実装してUserMailer.welcome_email
を呼び出します。
XxxMailerPreview
というクラスを作成し、それを規定の場所に配置するとメールのプレビューを見ることができるようになります。
実際のプレビューのクラスは次のような実装になります(こちらもRails Guideから引用)。
class UserMailerPreview < ActionMailer::Preview def welcome_email UserMailer.with(user: User.first).welcome_email end end
ActionMailer::Preview
を継承したクラスを作成し、メソッド定義の中でメールを送るメソッドを呼んでいるだけです。単純ですね。
この定義をすると、 http://localhost:3000/rails/mailers/user_mailer/welcome_email
にアクセスしてこのメールのpreviewを表示できます。
また、http://localhost:3000/rails/mailers
には定義されているpreviewが一覧されます。
なお有効にしたい環境のconfig/environments/ENV.rb
でconfig.action_mailer.show_previews = true
を定義すると、その環境でpreviewが有効となります。
デフォルトではdevelopment
環境のみで有効です。
つまりこの通りにpreviewクラスを実装していけば、Railsアプリケーションから送られるメールを一覧するページを実装できます。 ところが実際に実装していくにあたっていくつか考慮することがありました。
実際のデータを使う
Action Mailerのpreview機能は実際にメイラーを呼び出した結果をプレビューとして表示します。 つまりメイラーに渡す値は実際の値(もしくは実際の値のように振る舞うなにか)である必要があります。
今回preview機能を使用したRailsアプリケーションはマルチテナント型のアプリケーションで、かつテスト用のテナントを1つ持っています。 そのためメイラーのテストにはそのテスト用のテナントを使うようにしました。
ただし実際のデータを使うにあたって、特定の状態にあることを前提としたメイラーのプレビューを妥協しました。
たとえば「有料プランのトライアル期間が終了するn日前」に送られるメールがあるのですが、このメールを送るメイラーは対象のテナントがトライアル期間中であることを前提とした実装になっていました。 適当にデータを用意すればこのメイラーのプレビューも実装できるのですが、面倒だったので今回はデータを用意しませんでした。
ただし、プレビューを表示するメソッドのみは定義しておき、一覧ページにどのタイミングでメールが送られるかは表示されるようにしました。 メールのプレビューを見ようとするとエラーが出てしまいますが、「どのタイミングでメールが送られるか」が分かるだけでもマシだろうと判断しました。
認証
メイラーのプレビューページは/rails/mailers
にroutingされます。
デフォルトでは認証はかかっていないので、何も考えずにproduction
環境でプレビューを有効にすると一般ユーザーにも/rails/mailers
が露出してしまいます。
今回はこれを避けるため、production
環境ではプレビューを有効にせず、admin
環境でプレビューを有効にしました。
今回previewを導入したRailsアプリケーションはproduction
環境とは別にadmin
環境としても同じアプリケーションがホストされており、admin
環境には前段で認証がかかっています。
そのためadmin
環境でのみプレビューを有効にすることで、社内でのみプレビューを見れるようにできました。
ファイルの置き場
前述したとおり、デフォルトではプレビューの実装はtest/mailers/previews/**/*_preview.rb
として配置します。
ですがrspec-rails gemがインストールされていると、このデフォルトはspec/mailers/previews/**/*_preview.rb
に変更されます。
これは「環境によってrspec-railsがインストールされたりされなかったりする」場合に問題になります。
たとえばrspec-railsがdevelopment
, test
環境でのみインストールされる場合を考えます。
この場合はdevelopment
, test
環境ではデフォルト値がspec/
下なのですが、rspec-railsがインストールされないproduction
環境やadmin
環境ではデフォルト値がtest/
下になってしまいます。
これに最初気が付かず、デプロイしてみたら表示されるはずのプレビューが空になってしまいました。
spec/
下にファイルを置いていたのに、デプロイ先ではtest/
下を見ていたからですね。
これを回避するため、ファイルの置き場をapp/mailers/previews
下に変更しました。次のように変更できます。
# config/application.rb config.action_mailer.preview_path = Rails.root.join('app/mailers/previews').to_s
なお最初はrspec-railsが提供するデフォルト値を明示的に指定することも考えました。
ですが、このRailsアプリケーションが使用しているHeroku Review Appsでspec/
下をデプロイ対象から除くように指定しているため、spec/
下には置かないようにしました。
メソッド名
今回は、プレビュークラスのメソッド名を日本語で定義しました。
/rails/mailers
に表示されるのはプレビューのクラス名とメソッド名のみなので、メソッド名にすべての説明を詰め込む必要があります。
実装は次のようになっています。
module Previews module Notifications # ちなみにこのApplicationPreviewは自前で定義したActionMailer::Previewを継承するクラスで、 # Previewクラスで使う便利メソッド集を持っています。 class CommentNotificationMailerPreview < ApplicationPreview def 自分の投稿した記事にコメントされたとき # メイラーを呼び出すコード end def ウォッチしている記事にコメントされたとき # メイラーを呼び出すコード end def 自分がコメントした記事にコメントされたとき # メイラーを呼び出すコード end end end end
これは次のように表示されます。
RuboCopの設定
RuboCopは日本語のメソッドを定義すると怒ります。 そのため、プレビュークラスでは日本語のメソッドを定義しても怒らないよう、次の設定を追加しました。
# .rubocop.yml Naming/AsciiIdentifiers: Exclude: - app/mailers/previews/**/*.rb Naming/MethodName: IgnoredPatterns: - '[[:^ascii:]]'
Naming/AsciiIdentifiers
の設定は見てのとおりです。
Naming/MethodName
は少し説明が必要かもしれません。
このCopは「メソッド名がスネークケース(or キャメルケース)になっていること」を検査するCopです。
そしてその実装は、スネークケースを受理する正規表現にメソッド名がマッチするかをチェックしています。
ところが日本語にはスネークケースもなにもないので、日本語が含まれたメソッド名に対してこのCopは警告を出してしまいます。
そのため、IgnoredPatterns
を使用して、非ASCII文字が含まれていたら警告を出さないようにしています。
プレビュー定義の網羅を維持する
プレビュー機能を使うには、プレビュー用クラス定義をメイラーごとに実装する必要があります。
初回の実装ではすべてのメイラーに対してプレビュー用クラスを定義して回りました。 ですがこれを維持するには、メイラー定義が追加されるたびにプレビュー用クラスも追加しなければなりません。
今回は、プレビュー用クラスの追加を忘れないためにSiderとGoodcheckを活用しました。
Goodcheckは汎用Linterで、雑に言うとgrepです。 正規表現とそれに対応するメッセージを書くことで、正規表現にマッチしたコードに対して警告を出せます。 そしてSiderを使うと、Pull Requestごとに変更点のみにGoodcheckを実行できます。
この2つを使い、次の設定をして「メイラーが新規に定義された時」にプレビュー用クラスの定義を追加するよう警告を出すようにしました。
rules: - id: bitjourney.mail-preview pattern: regexp: 'def\s+[[:ascii:]&&[^\n]]+$' message: | メールの送信を追加する際には、app/mailers/previews/ 下にpreviewを追加してメールが送られるタイミングを記述してください。 justification: - すでにpreviewが書かれている場合 glob: - 'app/mailers/**/*.rb'
参考リンク
実装を読む際は次の2つのファイルを主に見ました。
- 実装: https://github.com/rails/rails/blob/v5.2.4.1/actionmailer/lib/action_mailer/preview.rb
ActionMailer::Preview
のdescendantsのpublic methodsを表示する、みたいな感じで分かりやすい実装です。
- 設定値: https://github.com/rails/rails/blob/v5.2.4.1/actionmailer/lib/action_mailer/railtie.rb
また、次のブログ記事を参考にしました。
- Action Mailer の基礎 - Railsガイド
- RailsのAction Mailer Previewsについて | 日々雑記
- ActionMailer Preview のススメ - Qiita
- 記事の内容には特に特筆すべきことはありませんが、スクショが貼られているのでどういう画面が作られるのかイメージしたい方は見ると良さそうです。
- Action Mailer Previewsをproduction環境で使えるようにする - そういうこともある
- 私の件と同じようなニーズである、production環境で非エンジニアにpreviewを見せる用途での記事です。そのため今回の実装にあたっていくらか参考にしました。
- GitHubのコードへのリンクがmasterブランチへのリンクになっているのが惜しいところ。
- また、記事中で
get "admin/rails/mailers" => "rails/mailers#index", internal: true
のように定義していますが、このinternal: true
はいらないのではないでしょうか。全然調べていないからわからないのですが、internal: true
はrake routes
した時にroutesに出さないためのRails内部のroutingだよという指定なのじゃないかなと予想しています。なのでユーザー定義のroutingには書かなくて良いと考えています。