✉️

ActionMailer の送信直前・直後に処理するコールバック

2021/11/26に公開

ActionMailer で送信直前に処理したいケースがあった。具体的には Amazon SES の秒間送信数制限にかからないように、必要なら sleep したい。

これをインスタンスメソッド内や before_action で行うと、deliver_later を呼ぶときにも処理が走ってしまい嬉しくないので、送信直前に処理を追加できるようにした。

実装の方針

deliver_later でも送信時には ActionMailer::MessageDelivery#deliver_now が呼ばれるので、下記のようにする。

  1. ActionMailer::Base.(before|after|around)_deliver でコールバックを定義できるようにする
  2. ActionMailer::MessageDelivery#(deliver_now|deliver_now!) で 1 で定義したコールバックを呼ぶ

実装

# config/initializers/action_mailer_deliver_callbacks.rb
# モンキーパッチなので動作確認したバージョンかチェック
raise "#{__FILE__} monkey patches actionmailer. Please check compatibility" unless ActionMailer.version.to_s == "6.1.3.2"

ActionMailer::Base.class_eval do
  define_callbacks :deliver

  class << self
    # AbstractController::Callbacks の実装を参考に before_deliver など定義
    [:before, :after, :around].each do |callback|
      define_method "#{callback}_deliver" do |*names, &blk|
        _insert_callbacks(names, blk) do |name, options|
          set_callback(:deliver, callback, name, options)
        end
      end
    end
  end
end

# rubocop:disable Style/ClassAndModuleChildren
module ActionMailer::MessageDelivery::Callbacks
  def deliver_now!
    processed_mailer.run_callbacks(:deliver) do
      super
    end
  end

  def deliver_now
    processed_mailer.run_callbacks(:deliver) do
      super
    end
  end
end
# rubocop:enable Style/ClassAndModuleChildren

ActionMailer::MessageDelivery.prepend(ActionMailer::MessageDelivery::Callbacks)

使い方

下記のように書ける。
※実際は複数プロセスで処理しても一定の頻度を超えないようにしました

class ApplicationMailer < ActionMailer::Base
  before_deliver do
    sleep 0.1
  end
end

Discussion