pockestrap

Programmer's memo

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を呼び出します。

https://railsguides.jp/action_mailer_basics.html#%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%AE%E3%83%97%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC

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.rbconfig.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-railsdevelopment, 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

これは次のように表示されます。

f:id:Pocke:20200425182055p:plain
プレビューのスクリーンショット

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を活用しました。

sider.review

github.com

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つのファイルを主に見ました。

また、次のブログ記事を参考にしました。

  • Action Mailer の基礎 - Railsガイド
  • RailsのAction Mailer Previewsについて | 日々雑記
    • Rails 4.1.1と古いですが、信頼の置けるソースです。
    • "production環境では動かない、というかdevelopment環境でしか動かない。"と書かれていますが、少なくともこの記事で対象としているRails 5.2.4.2ではdevelopment以外の環境でも使えるようになっています。
  • ActionMailer Preview のススメ - Qiita
    • 記事の内容には特に特筆すべきことはありませんが、スクショが貼られているのでどういう画面が作られるのかイメージしたい方は見ると良さそうです。
  • Action Mailer Previewsをproduction環境で使えるようにする - そういうこともある
    • 私の件と同じようなニーズである、production環境で非エンジニアにpreviewを見せる用途での記事です。そのため今回の実装にあたっていくらか参考にしました。
    • GitHubのコードへのリンクがmasterブランチへのリンクになっているのが惜しいところ。
    • また、記事中でget "admin/rails/mailers" => "rails/mailers#index", internal: trueのように定義していますが、このinternal: trueはいらないのではないでしょうか。全然調べていないからわからないのですが、internal: truerake routesした時にroutesに出さないためのRails内部のroutingだよという指定なのじゃないかなと予想しています。なのでユーザー定義のroutingには書かなくて良いと考えています。

次は今回プレビュー機能を使用したRailsアプリケーションです。

kibe.la