ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Railsでメール送信方法を自作してみる

Webエンジニアの本間です。 昨日、Rails 6で追加される予定の Action Text の概要をDHHが紹介している 動画 を見て、「おぉ、なんかすごい!」と思い紹介しようかと思ったのですが、いかんせん時間が少ないのであきらめます(^_^;)

今回は、少し前にRailsのメール送信を独自に実装したいケースがあり、その時に調査、実装した内容をサンプルコードを交えて紹介しようと思います。

自作のメール送信処理のサンプルとして、「Slackに通知するメール送信処理」を実装して見ます。 サンプルコードは action_mailer_my_delivery_method_test に格納していますので、サクッと試して見たい方は git clone して使って見てください。

詳細

実装方針

メール送信方法の自作の方針なのですが、ActionMailer::BaseのAPIドキュメントの configuration options の部分に重要な記述があります。

delivery_method - Defines a delivery method. ... Or you may provide a custom delivery method object e.g. MyOwnDeliveryMethodClass. See the Mail gem documentation on the interface you need to implement for a custom delivery agent.

Or you may provide a custom delivery method object と記載されているので、独自のメール送信方法が指定されることを想定していると考えて良さそうです。

See the Mail gem documentation on the interface you need to implement for a custom delivery agent. と記載があるため、Mail gemのインターフェースに沿ってメール送信方法を実装し、それをRailsで使うように設定すればいけると考えました。

メール送信処理の実装

Mail gemのインターフェースに沿って実装して、とのことですが、こちら明確なインターフェースがあるわけではなさそうです。 ただ、mail gem delivery_methods にある既存実装を参照する限り、以下の内容を実装すればよさそうです。

class MyMailDeliveryMethod
  # @return [Hash] メール送信の設定
  attr_accessor :settings

  # @param value [Hash] 
  def initialize(value)
    # TODO: 初期化処理の実装
  end

  # @param mail [Mail::Message]
  def deliver!(mail)
    # TODO: メール送信処理の実装
  end
end

サンプルの実装は以下のようになっています。 Slackへのメッセージ送信は slack-ruby-client を使っています。

SlackのAPI呼び出しに必要なトークンと送信先チャンネルは、初期化時の設定から取得するようにしています。 主にやっていることは「Mail::MessageオブジェクトをSlackにPostできる形式に変換」、「その結果をSlackへPost」の2つだけです。

require 'slack-ruby-client'

class SlackMailDeliveryMethod
  attr_accessor :settings

  def initialize(value)
    self.settings = value
  end

  def deliver!(mail)
    attachments = [to_attachment(mail)]
    channel = self.settings[:channel]

    client = Slack::Web::Client.new(token: self.settings[:api_token])
    client.chat_postMessage(channel: channel, attachments: attachments)
  end

  private

  def to_attachment(mail)
    {
      title: mail.subject,
      text: mail.body.to_s,
      fields: [
        { title: 'From', value: mail.from_addrs.to_s },
        { title: 'To', value: mail.to_addrs.to_s },
        { title: 'Cc', value: mail.cc_addrs.to_s },
        { title: 'Bcc', value: mail.bcc_addrs.to_s },
      ],
    }
  end
end

Railsでの設定

自作したメール送信方法をRailsで使うように設定する必要があります。 今回は、アプリケーション全体でデフォルトで使うメール送信方法に設定しました。 ここ以外にも、Mailerクラスごとや各メールで設定することができます。

以下の内容を記載したファイルを config/initializers/ 以下におき、起動時に設定します。

require "slack_mail_delivery_method"

# 独自のメール送信方法を追加する。
# これで.slack_settings=というメソッドが追加され、設定を保存できるようになる。
# この設定は、SlackMailDeliveryMethodの#initializeで渡される。
ActionMailer::Base.add_delivery_method(:slack, SlackMailDeliveryMethod)

# メール送信に必要な設定を行う。
ActionMailer::Base.slack_settings = {
  api_token: ENV['SLACK_API_TOKEN'],
  channel: ENV['SLACK_DEFAULT_CHANNEL'],
}

# デフォルトのメール送信方法に設定。
ActionMailer::Base.delivery_method = :slack

メール送信の実行

ここまでくれば、通常のActionMailerを使ったメール送信と同じです。

以下のようなMailerクラスを作成して、テストしてみます。

class TestMailer < ApplicationMailer
  default from: 'from@example.com'
  layout 'mailer'

  def test_email(channel = nil)
    mail to: "to@example.org",
         cc: 'cc@example.org',
         bcc: 'bcc@example.org',
         subject: 'テストメール',
         channel: channel
  end
end

そして、メール送信!

$ export SLACK_API_TOKEN=トークン文字列
$ export SLACK_DEFAULT_CHANNEL=通知先のチャンネル名(例: #general)

$ rails r 'TestMailer.test_email.deliver'

すると...、こんな感じで無事Slackに通知することができました。

f:id:ryu39:20181005101656p:plain

おまけ

非同期処理化

ActionMailerで実装することのメリットの1つとして、メール送信の非同期処理化が非常に簡単にできることが挙げられます(要ActiveJobの設定)。 やることは deliverdeliver_later にするだけです。

$ rails c

> TestMailer.test_email.deliver_later

# (使っているqueue_adapterにもよるが)ある程度時間をおいてからの送信も可能
> TestMailer.test_email.deliver_later(wait: 1.minute) # 1分後に実行

メール送信時に独自パラメーターを追加

メール送信処理に独自のパラメーターを渡すこともできます。 今回は、通知するチャンネルを変更できるようにしてみます。

まずMailerクラスの方で、mailメソッドに :channel パラメーターを追加してみます。

class TestMailer < ApplicationMailer
  # ....

  def test_email(channel = nil)
    mail to: "to@example.org",
         cc: 'cc@example.org',
         bcc: 'bcc@example.org',
         subject: 'テストメール',
         channel: channel # ここを追加!!
  end
end

次にメール送信のクラスの方で、指定されたチャンネルにPostするように実装を変更してみます。

class SlackMailDeliveryMethod
  # ...
  def deliver!(mail)
    # ...
    channel = mail['channel']&.value.presence || self.settings[:channel]
    # ...
    client.chat_postMessage(channel: channel, attachments: attachments)
  end
  # ...
end

ポイントは mail['channel']&.value です。これでMailerクラスでセットした :channel の値を取得することができます。 ただし注意点として、返ってくる値は文字列になっています。 これは、メールヘッダーに設定することを前提とした実装になっているためです。 ですので、文字列以外の値をセットする場合、取り扱いには注意してください。

それでは、チャンネルを変更してメール送信してみます。

$ rails r 'TestMailer.test_email("#another_my_channel")'

まとめ

今回、Railsに独自のメール送信方法を追加する方法を紹介しました。 Mail gemのインターフェースを実装し、それを既存実装に差し替えてセットするという方法は、とてもオブジェクト指向しているなー、という感覚があり楽しいです。

ActionMailerに関して、「ユーザーに何かを通知する」という方法は、10年前はメールがメインでしたが、現在はスマートフォンのプッシュ通知やブラウザのNotifications、SMSなど様々な方法があります。 アプリケーション内でこれらの通知の使い分けが必要になると、メールはActionMailer、プッシュ通知は独自実装、となって、コード内がややごちゃごちゃしてしまう、という感覚があります。 この辺をうまいこと抽象化できないかなー、と思ったり思わなかったりしています。

以上になります、最後まで読んできただきありがとうございました。