Ruby on Rails 7のAction MailerでSendgridを使う方法+List-Unsubscribe-Postヘッダー
はじめに
Sendgridの登録、APIキーの取得が終わっている状態からの説明となります。
APIキーはRailsで使えるようにcredentialsに登録しておきましょう。
メールテンプレートの作成方法やMailerの作成は、公式のドキュメントがあるため割愛します。
今回gemはsendgrid-rubyを使用します。
バージョン
rails: 7.0.8.1
sendgrid-ruby: 6.7.0
経緯
余談ですが、これまでプロジェクトではsendgrid-actionmailerというgemを使っていました。
昨今のGoogleのワンクリック配信停止の対応で、メールヘッダーにList-Unsubscribe-Postを設定する必要があったため改修していたところ、
上記のgemではメールヘッダーにList-Unsubscribe-Postを設定することができませんでした。
本gemはメンテナンスされていないこととスター数が少ないことからsendgrid-ruby gemへ乗り換えを実行しました。
結論ヘッダーも設定され既存機能にも影響のない形で乗り換えることができました。
全体のコード
配置するディレクトリはプロジェクトに合わせて変更してください。
# frozen_string_literal: true
require 'send_grid_mail'
ActionMailer::Base.add_delivery_method :sendgrid, SendGridMail, api_key: Rails.application.credentials.dig(:sendgrid, :api_key)
config.action_mailer.delivery_method = :sendgrid
# frozen_string_literal: true
class SendGridMail < ActionMailer::Base
require 'sendgrid-ruby'
include SendGrid
def initialize(settings)
@settings = settings
super()
end
def deliver!(mail)
personalization = personalization(mail)
sg_mail = sendgrid(mail, personalization)
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)
Rails.logger.info("SendGridMail#deliver! response: #{response.to_json}")
rescue StandardError => e
Rails.logger.error(e)
end
private
def personalization(mail)
personalization = SendGrid::Personalization.new
personalization.subject = mail.subject
Array(mail.to).each do |email|
personalization.add_to(SendGrid::Email.new(email:))
end
Array(mail.cc).each do |email|
personalization.add_cc(SendGrid::Email.new(email:))
end
Array(mail.bcc).each do |email|
personalization.add_bcc(SendGrid::Email.new(email:))
end
mail.header.fields.each do |field|
personalization.add_header(SendGrid::Header.new(key: field.name, value: field.value)) if %w[List-Unsubscribe-Post List-Unsubscribe].include?(field.name)
end
personalization
end
def sendgrid(mail, personalization)
sg_mail = SendGrid::Mail.new
sg_mail.from = SendGrid::Email.new(email: mail.from)
sg_mail.subject = mail.subject
sg_mail.add_content(SendGrid::Content.new(type: 'text/html', value: mail.body.parts[1].body.raw_source))
sg_mail.add_personalization(personalization)
if mail[:categories].present?
mail[:categories].unparsed_value.each do |category|
sg_mail.add_category(SendGrid::Category.new(name: category))
end
end
sg_mail
end
end
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe SendGridMail do
let(:settings) { { api_key: 'YOUR_API_KEY', api_host: 'YOUR_API_HOST' } }
let(:mail) do
Mail.new do
from 'from@example.com'
to 'to@example.com'
body 'Test Body'
html_part do
body 'Test Body'
end
end
end
let(:sendgrid_mail) { SendGrid::Mail.new }
before do
allow(SendGrid::API).to receive(:new).and_return(double(client: double(mail: double(_: double(post: true))))) # rubocop:disable RSpec/VerifiedDoubles
allow(SendGrid::Mail).to receive(:new).and_return(sendgrid_mail)
end
describe '#initialize' do
it 'sets the settings' do
send_grid_mail = described_class.new(settings)
expect(send_grid_mail.instance_variable_get(:@settings)).to eq(settings)
end
end
describe '#deliver!' do
let(:send_grid_mail) { described_class.new(settings) }
before do
allow(send_grid_mail).to receive(:deliver!).and_return(true)
end
it 'sends the mail using SendGrid API' do
expect(send_grid_mail.deliver!(mail)).to be(true)
end
it 'raises an error when the API call fails' do
allow(send_grid_mail).to receive(:deliver!).and_raise(StandardError)
expect { send_grid_mail.deliver!(mail) }.to raise_error(StandardError)
end
end
describe '#personalization' do
it 'returns a SendGrid personalization object' do
send_grid_mail = described_class.new(settings)
personalization = send_grid_mail.send(:personalization, mail)
expect(personalization).to be_a(SendGrid::Personalization)
expect(personalization.subject).to eq(mail.subject)
end
it 'adds recipients to personalization' do
mail.to = ['to1@example.com', 'to2@example.com']
mail.cc = ['cc1@example.com', 'cc2@example.com']
mail.bcc = ['bcc1@example.com', 'bcc2@example.com']
send_grid_mail = described_class.new(settings)
personalization = send_grid_mail.send(:personalization, mail)
expect(personalization.tos).to eq([{ 'email' => 'to1@example.com' }, { 'email' => 'to2@example.com' }])
expect(personalization.ccs).to eq([{ 'email' => 'cc1@example.com' }, { 'email' => 'cc2@example.com' }])
expect(personalization.bccs).to eq([{ 'email' => 'bcc1@example.com' }, { 'email' => 'bcc2@example.com' }])
end
it 'adds headers to personalization' do
mail.header['List-Unsubscribe-Post'] = 'unsubscribe-post@example.com'
mail.header['List-Unsubscribe'] = 'unsubscribe@example.com'
send_grid_mail = described_class.new(settings)
personalization = send_grid_mail.send(:personalization, mail)
expect(personalization.headers).to eq(
{ 'List-Unsubscribe-Post' => 'unsubscribe-post@example.com',
'List-Unsubscribe' => 'unsubscribe@example.com' }
)
end
end
describe '#sendgrid' do
before do
allow(SendGrid::Email).to receive(:new).and_return(sendgrid_mail.from)
end
it 'returns a SendGrid mail object' do
send_grid_mail = described_class.new(settings)
sg_mail = send_grid_mail.send(:sendgrid, mail, SendGrid::Personalization.new)
expect(sg_mail).to be_a(SendGrid::Mail)
expect(sg_mail.from).to eq(SendGrid::Email.new(email: 'from@example.com'))
expect(sg_mail.subject).to eq(mail.subject)
expect(sg_mail.contents).to eq([{ 'type' => 'text/html', 'value' => 'Test Body' }])
expect(sg_mail.personalizations).to eq([{}])
end
it 'adds categories to SendGrid mail' do
mail[:categories] = %w[category1 category2]
send_grid_mail = described_class.new(settings)
sg_mail = send_grid_mail.send(:sendgrid, mail, SendGrid::Personalization.new)
expect(sg_mail.categories).to eq(%w[category1 category2])
end
end
end
以上でサーバーの起動と同時にSendGridMailが初期化され、ActiomMailerとして使用することができます。おまけにRspecを置いておきます。
詰まったところ
ローカルで動かして取れる値と、実際のサーバーで動かして取れる値に違いがあり、
何度もデバッグしないといけないところが大変でした。
具体的な箇所で言うと
mail.header.fields.each do |field|
personalization.add_header(SendGrid::Header.new(key: field.name, value: field.value)) if %w[List-Unsubscribe-Post List-Unsubscribe].include?(field.name)
end
や
sg_mail.add_content(SendGrid::Content.new(type: 'text/html', value: mail.body.parts[1].body.raw_source))
この部分がプロジェクトごと(メールテンプレートやheadersの設定方法)によって変わってくると思われるので注意してください。
成果
成功するとメールヘッダーに値が設定されていることが確認できました。
メールタイトル横に「メーリングリストの登録解除」というボタンがGmailでは表示されるとのことですが、検証環境などでは表示されませんでした。
このボタンに関してはネットにいろいろと情報がありますが、しっかりとヘッダーが設定されていれば問題ないと思われるので引き続き状況を監視してみたいと思います。
Discussion