開発部の平野(@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
環境以外の環境では:sendgrid
のdelivery_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への移行の話や、ランサーズの新卒エンジニアがインターン入社してからどのようなタスクに取り組んできたかについて、お話する予定ですので是非興味を持たれた方はご参加ください!