ActionMailer の perform_deliveries を false にしても、メール配信が停止しないパターンがある件について
自分が携わっているプロダクトにはメール配信機能があるのですが、特定のお客様へのメール配信を全て停止するイレギュラーな仕様がありました。しかし、その特定のお客様に「実際にはメール配信が行われていた」というインシデントが発生しました。
このインシデントを調査する中で、学びがあったためその内容を共有したいと思い、この記事を書きました。
原因
既存のメール配信停止を行う処理を確認したところ、全ての Mailer クラスの親である ApplicationMailer クラスの after_action コールバックにて、特定の条件の時にのみ perform_deliveries を false にすることでメール配信が停止するようになっていました。
さらに、perform_deliveries に関しては、以下のような説明が Rails ガイドに記載されており、今回実現したいことにマッチしているように見えました。
Mailのメッセージにdeliverメソッドを実行したときに実際にメール配信を行なうかどうかを指定します。デフォルトでは配信が行われます。機能テストなどで配信を一時的にオフにしたい場合に便利です
# イメージ
class ApplicationMailer < ActionMailer::Base
after_action :abort_sending
private
def abort_sending
mail.perform_deliveries = false unless send?(mail)
end
end
ところが、ActionMailer の中身を確認したところ、丁寧に以下のコメントがありました。。
なんと、deliver_now! を実行した時には perform_deliveries の値が無視されるのです😭
つまり、これが今回のインシデントの原因でした!
さらに深掘り
メール送信自体は、gem mail の責務となるため、そちらも確認しました。
deliver_now! は内部で mail#deliver! を呼び出しますが、この deliver! メソッドの中では perform_deliveris を参照していないことが確認できました。
一方で、deliver_now の内部で呼ばれる mail#deliver(実際には mail#do_delivery)では、perform_deliveries を参照してメール送信の可否を判断していることが確認できました。
このことから、deliver_now では perform_deliveries が有効であり、deliver_now! では無効であることが分かりました。
暫定対応
プロダクト内には deliver_later! や deliver_now! を使用している箇所が 100 か所以上あり、全てを把握して修正するには時間がかかりすぎると判断しました。
そのため、以下のようにモンキーパッチを当て、deliver_now! でも perform_deliveries を参照するようにしました。
ActiveSupport.on_load(:action_mailer) do
module ActionMailer
class MessageDelivery
# 元の処理: https://github.com/rails/rails/blob/2fe1ca0f058c3e9ffd7fa7616e27c03aed8dba7a/actionmailer/lib/action_mailer/message_delivery.rb#L111-L117
def deliver_now!
return self unless perform_deliveries
processed_mailer.handle_exceptions do
processed_mailer.run_callbacks(:deliver) do
message.deliver!
end
end
end
end
end
end
そもそもどうすべきだったのか?
deliver_now! や deliver_later! を利用している箇所を調査したところ、実行時の例外をハンドリングしたいという意図がありました。
しかし、個人的には deliver_now や deliver_later で十分だと思います。というのも、mail#do_delivery 内では raise_delivery_errors が true の場合、例外を外に投げる仕組みになっているからです。
また、raise_delivery_errors は Rails ガイドにも記載があるようにデフォルトで true になっています。
つまり、deliver_now でも deliver_now! と同様に例外をキャッチできるわけです。
さいごに
今回のインシデントを通じて、ActionMailer の内部処理について理解を深めることができ、非常に勉強になりました。
Discussion
あーーー、これRailsチュートリアルのコードでも同じことが言えそうですね... 💦 調査してみて、問題なければ更新してみようかと思います。分かりやすい記事ありがとうございます...!! 🙏✨