ぶちのブログ

競プロとCTFが趣味なWebエンジニアのアウトプットの場

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が違うため、命名ディレクトリ構成が悩ましい。 設計を頑張ることと、現実的な実装速度との兼ね合いでこういう形に落ち着いた。