Railsのメール送信をSendGridに移行しました

hirano.tatsuya|2021年09月01日
Rails

開発部の平野(@44511336tatuya)です。今回はRuby on Rails製のLancers Creative(以下LCC)のメールサーバーをsmtpからSendGridに移行しました。その際、課題や実装した内容などご紹介させて頂こうと思います!

なぜsmtpからSendGridに移行したのか

今まではgmailのアカウントを使ってsmtp経由でメールを送信していたのですが、いくつかの問題を抱えていました。

  • 存在しないメールアドレスにメール送信をするとスパム認定されメール送信が一時的にストップしてしまう
  • 一日のメール送信上限が限られている

このような問題が起こっていましたので、今回の移行に踏み切りました。

Railsアプリからメール送信

SendGridを使ったメール送信には「Web API」と「SMTP」の2種類の方法がありますが、今回はランサーズ本家でも使用しているWeb APIでの実装にしました。また今までのActionMailerの各メーラーでの処理はそのまま流用し、ActionMailerの配信方法(deliver_method)をSendGrid Web API用に新しく追加する形を取りました。

公式でgem sendgrid-rubyが用意されているのでこちらを使用して実装していきます。


実装内容

  • ディレクトリ構成
config
├── initializers
│ ├── mail.rb
│ ├── sendgird.rb
├── mail.yml
├── mail.yml.example
lib
├── mail
│ └── send_grid.rb

1.  SendGridアカウント及びAPI Keyの作成

まずはSendGridのアカウントとAPI Keyの作成を公式の手順通り行います。

https://sendgrid.kke.co.jp/docs/Tutorials/A_Transaction_Mail/manage_api_key.html

2. Gemfileにsendgrid-rubyを追加

  • Gemfile
gem 'sendgrid-ruby'

3. initialize時にActionMailerのdelivery_methodを拡張

ActionMailerがdefaultで用意している配信方法(deliver_method)は以下の4つとなります。詳細はRailsガイド を参照下さい。

  • :smtp
  • :sendmail
  • :file
  • :test

これらの配信方法以外にも独自の配信方法を追加することができ、ActionMailerが用意しているadd_deliver_method というメソッドを使用して、以下の様なで:sendgridという配信方法を新たに追加します。

  • config/initializers/sendgird.rb
Rails.configuration.to_prepare do
  ActionMailer::Base.add_delivery_method :sendgrid, Mail::SendGrid, api_key: ENV['SENDGRID_API_KEY']
end

上記のファイルが initialize時に呼ばれ、deliver_method:sendgridが追加されます。第2引数で渡しているclassは、後述する5の  lib/mail/sendgrid.rbで作成するMail::SendGridクラスになります。

また、この際に1で作成し環境変数化したAPI Keyも引数として渡しておきます。

4. mail.ymlを書き換える

LCCのメール設定はmail.ymlという1つのファイルで各環境毎の設定を管理しております。このファイルをinitialize時に呼ばれるmail.rbで読み込むことで、環境に応じた配信方法等のメール設定をしています。今回の実装では、:smtpを指定していたところを3で追加した:sendgridに書き換えました。

  • mail.yml
development:
  delivery_method: sendgrid
  raise_delivery_errors: true
  default_url_options:
    host: localhost:3000

test:
  delivery_method: test
  raise_delivery_errors: true
  default_url_options:
    host: localhost:3000

production:
  delivery_method: sendgrid
  raise_delivery_errors: true
  default_url_options:
    host: "本番環境ホストURL"

pre:
  delivery_method: sendgrid
  raise_delivery_errors: true
  default_url_options:
    host: "pre環境ホストURL"
  • mail.rb
MAIL_CONFIG = YAML.load_file(File.expand_path(Rails.root.join("config", "mail.yml"), __FILE__))[Rails.env]
Rails.application.config.action_mailer.delivery_method = MAIL_CONFIG['delivery_method'].to_sym
Rails.application.config.action_mailer.raise_delivery_errors = MAIL_CONFIG['raise_delivery_errors']
Rails.application.config.action_mailer.default_url_options = MAIL_CONFIG['default_url_options']

これでtest環境以外の環境では:sendgriddelivery_methodが適用され、add_delivery_method時に引数で渡していたMail::SendGridクラスがメール送信時に呼ばれます。

5.deliver!メソッドをオーバーライド

deliver!メソッドをオーバーライドすることで、既存の各メーラーで作成されたメールオブジェクトが実行されたタイミングでこちらのクラスが呼ばれます。このクラス内での処理としては、以下の3点になります。

  • 既存の各メーラーで作成されたメールオブジェクトをSendGrid用のメールオブジェクトに置き換えていく
  • API呼び出し
  • API呼び出し時の接続先ホスト(エンドポイントのホスト部)の切り替え(こちらについては後ほど説明します)

sendgrid-rubyが用意するヘルパークラスを用いて以下の様に記述していきます。

  • lib/mail/send_grid.rb
class Mail::SendGrid
  # settingsにはsendgrid.rbで渡したapi_keyが格納されている
  # 下記プライベートメソッドのset_api_hostで取得した値も追加しておき、API呼び出し時に引数として渡す
  def initialize(settings)
    settings[:api_host] = set_api_host
    @settings = settings
  end

  def deliver!(mail)
    # ActionMailerのメイラー内で作成したメールオブジェクトの詳細情報をSendGrid用のメールオブジェクトに置き換えていく
    # personalizationでto, cc, bccを設定
    personalization = ::SendGrid::Personalization.new
    personalization.subject = mail.subject

    Array(mail.to).each do |email|
      personalization.add_to(::SendGrid::Email.new(email: email))
    end

    Array(mail.cc).each do |email|
      personalization.add_cc(::SendGrid::Email.new(email: email))
    end

    Array(mail.bcc).each do |email|
      personalization.add_bcc(::SendGrid::Email.new(email: email))
    end

    # from, subject, content, personalizationを設定しSendGrid用のメールオブジェクト完成
    sg_mail = ::SendGrid::Mail.new
    sg_mail.from = ::SendGrid::Email.new(email: mail.from.first)
    sg_mail.subject = mail.subject
    sg_mail.add_content(::SendGrid::Content.new(type: 'text/plain', value: mail.body.raw_source))
    sg_mail.add_personalization(personalization)

    # API呼び出し
    sg = ::SendGrid::API.new(api_key: @settings[:api_key], host: @settings[:api_host])
    response = sg.client.mail._('send').post(request_body: sg_mail.to_json)
    puts response.status_code
    puts response.headers
  end

  private

  # pre及びdevelopment環境では各環境のモックコンテナへリクエストを送りたいので、環境毎にAPI接続先のホストを取得する(後ほど説明)
  def set_api_host
    return 'https://api.sendgrid.com' if Rails.env == "production"
    return 'https://pre-sendgrid-creativecloud.lancers.jp' if Rails.env == "pre"
    return 'https://localhost:3030' if Rails.env == "development"
  end
end

deliver!の引数であるmailには、ActionMalerの各メーラーで作成されたメールオブジェクトが渡ってきます。各メーラーでは下記の様にmailにtoやsubjectが設定されますが、その設定情報を上記でSendGrid用のメールオブジェクトに置き換えています。

Class UserMailer < ApplicationMailer
  default from: 'hoge@example.com'

  def foo
    @user = params[:user]
    mail(to: @user.email,
         subject: '件名')
  end
end

メール送信テスト

 

開発環境及びStg環境(Pre環境)では実際にSendGridのAPIにリクエストは送らず、モックコンテナを用いてテストを行います。モックコンテナで用意したモックAPIにリクエストを送ると、上図のsendgrid-devからmaildevへsmtpでメール配信され、http://xxx.xxx.xxx.xxx:1080/(開発環境)にアクセスすることで、maildevへメールが送信されていることがブラウザ上から確認できます。

モックコンテナについての詳細は SendGrid用のMailモックコンテナを作りました でエンジニアブログに記載しておりますので、ご参照下さい。

アプリ側の実装として、API接続先の切り替えは以下の手順となります。

  • lib/mail/send_grid.rb

環境毎にAPI接続先のホスト情報を取得します。

 private

  # pre及びdevelopment環境では各環境のモックコンテナへリクエストを送りたいので、環境毎にAPI接続先のホストを取得する
  def set_api_host
    return 'https://api.sendgrid.com' if Rails.env == "production"
    return 'https://pre-sendgrid-creativecloud.lancers.jp' if Rails.env == "pre"
    return 'http://localhost:3030' if Rails.env == "development"
  end

set_api_hostで取得した値を@settingsに渡しておきます。

  # settingsにはsendgrid.rbで渡したapi_keyが格納されている
  # 下記プライベートメソッドのset_api_hostで取得した値も追加しておき、API呼び出し時に引数として渡す
  def initialize(settings)
    settings[:api_host] = set_api_host
    @settings = settings
  end

API呼び出し時に引数として渡します。

  sg = ::SendGrid::API.new(api_key: @settings[:api_key], host: @settings[:api_host])

まとめ

今回はsmtpからSendGrid Web APIへの移行が中心となりましたが、SendGrid Web APIを使用することにより、各メールへのカテゴリー設定やEvent Webhookとの連携ができる様になります。これらの便利な機能の導入も進めていきたいと思います!


お知らせ

2021/9/8日に【Lancers x SUPER STUDIO】Engineer Meetup #1を開催します。今回実装したSendGridへの移行の話や、ランサーズの新卒エンジニアがインターン入社してからどのようなタスクに取り組んできたかについて、お話する予定ですので是非興味を持たれた方はご参加ください!