Railsで、特定ドメインや特定メアドにはメールを送らないことで、staging環境での事故を防ぐ実装例
はじめに
「develop環境では開発者のみに、staging環境ではテストなどの関係者のみにメールを送りたい」もしくは、「本番サイトでも関係者のみにメールを送りたい」
などの要望がありましたが、データベース側では制御が難しいという状況に至りました。
このような場合に、Rails側で簡単にかつ漏れなくメールの宛先をハンドリングする必要があります。
このような実装が、テストを含めかなりうまくまとまったため、公開します。
方針
すべてのメールに対してのフックなので、ActionMailerInterceptorを用いるのが妥当。
あとは、Rubyらしく書きながら、拡張性が高くなるように、Procオブジェクトをvalidatorにして実装した。
実装
class ActionMailerInterceptor VALID_DOMAINS = %w[@hoge.co.jp @fuga.co.jp].freeze DOMAIN_VALIDATOR = ->(address) { address.end_with?(*VALID_DOMAINS) } VALID_ADDRESS = %w[hoge@hoge.co.jp hoga@hoge.co.jp].freeze ADDRESS_VALIDATOR = ->(address) { VALID_ADDRESS.include?(address) } FOR_ALL_VALIDATOR = -> { true } FOR_NONE_VALIDATOR = -> { false } class << self def delivering_email(message) set_validator message.to = validate(message.to) message.cc = validate(message.cc) if message.cc end private def set_validator @validator = if Rails.env.production? FOR_ALL_VALIDATOR elsif Rails.env.staging? DOMAIN_VALIDATOR elsif Rails.env.development? DEVELOPER_VALIDATOR else FOR_NONE_VALIDATOR end end def validate(addresses) addresses.select { |to| @validator.call(to) } end end end ActionMailer::Base.register_interceptor(ActionMailerInterceptor)
validatorをlambdaで定義することによって、メソッドとして定義するよりもスッキリと書けます。
特定の場合にはログを吐きたい等の場合にも、ブロック内に適当なloggerを仕込むことで実現できます。
テストコード(一部)
describe ActionMailerInterceptor do let(:mock_message_class) { Struct.new(:to, :cc) } let(:message) { mock_message_class.new(to, cc) } context 'when staging' do context 'with appropriate to and cc addresses' do let(:to) { ['c@hoge.co.jp'] } let(:cc) { ['d@fuga.co.jp'] } it 'does not change addresses' do allow(Rails).to receive(:env).and_return('staging'.inquiry) described_class.delivering_email(message) expect(message).to eq( mock_message_class.new(['c@hoge.co.jp'], ['d@fuga.co.jp']) ) end end end end
toとccに対してstring[]を返すMockMessageClassをStructで定義することで、容易にテストが書ける。
メールのような機能はテストをかなり厚くしたいが、現実的な記述量で網羅的なテストが書けそう。
利点・問題点
スッキリと書けており、場合分けも一箇所にまとまっていて、可読性が高い。 Interceptorが厳密に単一の責務を果たしてはいない。本来はValidatorクラスを切り出したほうが、設計としてはよりきれいかもしれない。 一方、ModelのValidatorとはInterfaceが違うため、命名やディレクトリ構成が悩ましい。 設計を頑張ることと、現実的な実装速度との兼ね合いでこういう形に落ち着いた。