2017-08-02

RailtieでActionMailerを拡張する

Railtieを使ってActionMailerのdelivery_methodを拡張してみました。 サンプルで作ったgemはこちら↓

任意のURLにHTTP POSTするdelivery_methodを定義したgemになります。

ということで今回は作り方の備忘として残します。

作り方

Rails::Railtieのサブクラスで定義したinitializerのブロックをRailsが自動で呼び出す仕組みになっています。このinitializerブロック内に拡張する処理を入れていきます。

http_action_mailerの例だと以下のようにActionMailer::BaseがロードされたタイミングでActionMailer::Base#add_delivery_methodでdelivery_methodを追加しています。

# lib/http_action_mailer/delivery_method.rb
require 'http_action_mailer/delivery_method'

module HttpActionMailer
  class Railtie < Rails::Railtie
    initializer 'http_action_mailer.add_delivery_method', before: 'action_mailer.set_configs' do
      ActiveSupport.on_load :action_mailer do
        ActionMailer::Base.add_delivery_method(:http, HttpActionMailer::DeliveryMethod)
      end
    end
  end
end

delivery_methodに追加するクラスはこんな感じで定義します。基本的にinitializeとdelivery!メソッドを定義すれば良いです。

require 'faraday'
require 'json'

module HttpActionMailer
  class DeliveryMethod
    def initialize(settings)
      @settings = settings
    end

    def deliver!(mail)
      http_post(mail)
    end

    private

    def http_post(mail)
      conn = Faraday.new(url: @settings[:url])
      conn.post do |r|
        r.path = "#{@settings[:path]}/#{Array(mail.to).first}"
        r.headers = @settings[:headers] if @settings[:headers]
        r.body = {
          from: mail.from,
          to: mail.to,
          cc: mail.cc,
          subject: mail.subject,
          text: mail.text_part.body.raw_source,
          html: mail.html_part.body.raw_source,
        }.to_json
      end
    end
  end
end

deliver!の引数にはMail::Messageのオブジェクトが渡されます。コンストラクタの引数にはconfig.xxx_settingsで設定したハッシュが渡されます。xxxの部分にはadd_delivery_methodの第一引数のシンボルの名前が入ります。http_action_mailerの例だとconfig.http_settings = { ... } といった感じで設定できます。xxx_settingsのアクセサはadd_delivery_method呼び出し時に自動的に生成されます。add_delivery_methodの定義はこんな感じです↓

module ActionMailer
  module DeliveryMethods
    module ClassMethods
      def add_delivery_method(symbol, klass, default_options = {})
        class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings")
        send(:"#{symbol}_settings=", default_options)
        self.delivery_methods = delivery_methods.merge(symbol.to_sym => klass).freeze
      end
...

あとはrequire 'xxx'でRailtieがロードされるように調整します

# lib/http_action_mailer.rb
require 'http_action_mailer/railtie' if defined?(Rails)

gem化する場合は、generatorを使ってスケルトン作って、lib直下にRailtieを書けばOK

$ rails plugin new http_action_mailer --skip-bundle

実際のRailsアプリでテストをしたい場合はtest/dummy配下にRailsアプリが作られているので、コントローラやルーティングを設定してアプリを立ち上げれば、アプリの手動テストができます。

テストコードにrspecを使いたい場合はこちらを参照すると良いです↓

before: 'action_mailer.set_configs'について

Railtieのinitializerのオプションで before: 'action_mailer.set_configs'を入れないと、config.xxx_settings での設定ができません。set_configsでもActionSupport.on_loadを使ってActionMailer::Baseのロードをフックしていますが、add_delivery_methodをする前にset_configsのhookブロックが呼び出されるとxxx_settingsのメソッドがまだ定義されていないためエラーになります。そのためset_configsの前にadd_delivery_methodを呼び出すブロックをon_loadで渡してあげることで、xxx_settingsのアクセサ定義 → send("xxx_settings")呼び出しという流れでdelivery_methodの設定をすることができます。
module ActionMailer
  class Railtie < Rails::Railtie # :nodoc:
    ...
    initializer "action_mailer.set_configs" do |app|
    ...
      ActiveSupport.on_load(:action_mailer) do
      ...
        options.each { |k, v| send("#{k}=", v) }
      end      
...
end

参考URL

このエントリーをはてなブックマークに追加