久々にメール送信の実装を行ったので、メモしておきます。
難しくはないけど、fromに名前入れたり、マルチパートメールにしたりも合わせて対応。
メールのSpecに初挑戦。ActionMailer Previewなるものもあったので、合わせて対応しました。
メーラー作成
先ずは、rails generateで自動生成します。
$ rails g mailer UserMailer
create app/mailers/user_mailer.rb
invoke erb
create app/views/user_mailer
create app/views/layouts/mailer.text.erb
invoke rspec
create spec/mailers/user_mailer_spec.rb
create spec/mailers/previews/user_mailer_preview.rb
【参考】ここまでのコミット内容
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/619fc531c1ebfd75ff61332381039c0147c26014
コントローラーから呼び出し
アカウント削除の予約や取り消しは、別タスクで実装済なので、メール送信処理のみ加えます。
app/controllers/users/registrations_controller.rb に追加
# DELETE /users アカウント削除(処理)
def destroy
resource.set_destroy_reserve
+ UserMailer.with(user: current_user).destroy_reserved.deliver_now
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message! :notice, :destroy_reserved
yield resource if block_given?
respond_with_navigational(resource) { redirect_to after_sign_out_path_for(resource_name) }
end
# PUT /users/undo_destroy アカウント削除取り消し(処理)
def undo_destroy
resource.set_undo_destroy_reserve
+ UserMailer.with(user: current_user).undo_destroy_reserved.deliver_now
set_flash_message! :notice, :undo_destroy_reserved
redirect_to root_path
end
呼び出されるメーラー
共通化の為、実処理は継承元のApplicationMailerにメソット作って、タイトルのキーのみ渡します。
使用されるメールテンプレは、暗黙的に呼び出し元のメソット名が使われます。
app/mailers/user_mailer.rb に追加
class UserMailer < ApplicationMailer
+ # アカウント削除受け付けのお知らせ
+ def destroy_reserved
+ send_mail('mailer.user.destroy_reserved.subject')
+ end
+
+ # アカウント削除取り消し完了のお知らせ
+ def undo_destroy_reserved
+ send_mail('mailer.user.undo_destroy_reserved.subject')
+ end
end
app/mailers/application_mailer.rb に追加
class ApplicationMailer < ActionMailer::Base
layout 'mailer'
+ private
+
+ # メール送信
+ def send_mail(subject_key)
+ @user = params[:user]
+ mail(from: "\"#{Settings['mailer_from']['name'].gsub(/%{app_name}/, t('app_name'))}\" <#{Settings['mailer_from']['email']}>",
+ to: @user.email,
+ subject: t(subject_key).gsub(/%{app_name}/, t('app_name')))
+ end
end
定数定義
メーラーで呼び出している定数を定義します。
config/settings/development.yml に追加
mailer_from:
name: '%{app_name}'
email: 'noreply@localhost'
※test.ymlやproduction.ymlも同様に定義
config/locales/ja.yml に追加
ja:
app_name: "RailsAppOrigin"
+ user:
+ destroy_reserved:
+ subject: "【%{app_name}】アカウント削除受け付けのお知らせ"
+ undo_destroy_reserved:
+ subject: "【%{app_name}】アカウント削除取り消し完了のお知らせ"
メール本文
メーラーで使用される本文のテンプレを追加します。
app/views/user_mailer/destroy_reserved.html.erb を作成
<%= @user.email %> 様<br/>
<%= t('app_name') %> をご利用頂きありがとうございます。<br/>
<br/>
アカウント削除を受け付けました。またのご利用をお待ちしております。<br/>
<br/>
このアカウントは<%= l(@user.destroy_schedule_at.to_date) %>以降に削除されます。それまでは取り消し可能です。<br/>
削除されるまではログインできますが、一部機能が制限されます。<br/>
<%= link_to 'アカウント削除取り消し', users_undo_delete_url %><br/>
text.erbを作成すると、マルチパートメールになります。
app/views/user_mailer/destroy_reserved.text.erb を作成
<%= @user.email %> 様
<%= t('app_name') %> をご利用頂きありがとうございます。
アカウント削除を受け付けました。またのご利用をお待ちしております。
このアカウントは<%= l(@user.destroy_schedule_at.to_date) %>以降に削除されます。それまでは取り消し可能です。
削除されるまではログインできますが、一部機能が制限されます。
アカウント削除取り消し
<%= users_undo_delete_url %>
Tips: 最初、xxx_urlをxxx_pathと書いてしまいエラーになりました。
スキーマ+ドメインがないとアクセスできないので当然で、よく考えると親切ですね。
app/views/user_mailer/undo_destroy_reserved.html.erb を作成
<%= @user.email %> 様<br/>
<br/>
<%= t('app_name') %> をご利用頂きありがとうございます。<br/>
<br/>
アカウント削除の取り消しが完了しました。<br/>
上記と同様に、text.erbを作成すると、マルチパートメールになります。
app/views/user_mailer/undo_destroy_reserved.text.erb を作成
<%= @user.email %> 様
<%= t('app_name') %> をご利用頂きありがとうございます。
アカウント削除の取り消しが完了しました。
動作確認
$ rails s -> http://localhost:3000/
Spec実装
初めてなので、テスト駆動ではなく、後から書きました。
spec/mailers/user_mailer_spec.rb に追加
# アカウント削除受け付けのお知らせ
describe 'destroy_reserved' do
let(:user) { FactoryBot.create(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days) }
let(:mail) { UserMailer.with(user: user).destroy_reserved }
it '送信者のメールアドレスが設定と一致' do
expect(mail.from).to eq([Settings['mailer_from']['email']])
end
it '宛先がユーザーのメールアドレスと一致' do
expect(mail.to).to eq([user.email])
end
it '本文(html)にアカウント削除取り消しのURLが含まれる' do
expect(mail.html_part.body).to include("\"#{users_undo_delete_url}\"")
end
it '本文(text)にアカウント削除取り消しのURLが含まれる' do
expect(mail.text_part.body).to include(users_undo_delete_url)
end
end
# アカウント削除取り消し完了のお知らせ
describe 'undo_destroy_reserved' do
let(:user) { FactoryBot.create(:user) }
let(:mail) { UserMailer.with(user: user).undo_destroy_reserved }
it '送信者のメールアドレスが設定と一致' do
expect(mail.from).to eq([Settings['mailer_from']['email']])
end
it '宛先がユーザーのメールアドレスと一致' do
expect(mail.to).to eq([user.email])
end
end
動作確認
$ rspec spec/mailers/user_mailer_spec.rb 6 examples, 0 failures
ActionMailer Preview
追伸:FactoryBot.createだとTruncateされないので、build_stubbedに変更しました。
[rails s]で動くので、testではなくdevelopmentのDBですね。
seedする筈ではあるが、必ずユーザーが居るとは限らないので、FactoryBotは使いたい。
spec/mailers/previews/user_mailer_preview.rb に追加
# アカウント削除受け付けのお知らせ def destroy_reserveduser = FactoryBot.create(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days)user = FactoryBot.build_stubbed(:user, destroy_schedule_at: Time.current + Settings['destroy_schedule_days'].days) UserMailer.with(user: user).destroy_reserved end # アカウント削除取り消し完了のお知らせ def undo_destroy_reserveduser = FactoryBot.create(:user)user = FactoryBot.build_stubbed(:user) UserMailer.with(user: user).undo_destroy_reserved end
動作確認
送信しなくても、見れるので、作成・変更した時の確認に便利ですね。
宛先と本文のメールアドレスが異なるけど、iframe使ってて、別々にリクエストされるから。
(大事な所ではないので、拘らずにこのままにしました)
$ rails s -> http://localhost:3000/rails/mailers
導線追加
リンクがページにあると便利なので、フッターの下に追加。開発環境のみ表示。
序でにLetterOpenerWebも。
app/views/layouts/application.html.erb に追加
<% if Rails.env.development? %>
<ul>
<li><%= link_to '[LetterOpenerWeb]', letter_opener_web_path, target: :_blank, rel: [:noopener, :noreferrer] %></li>
<li><%= link_to '[ActionMailer Preview]', '/rails/mailers', target: :_blank, rel: [:noopener, :noreferrer] %></li>
</ul>
<% end %>
Tips: noopenerとnoreferrerは、セキュリティとパフォーマンスの為に追加した方がいい。
(自サイト内なので、気にしなくても大丈夫ですが、念の為、入れてます)
参考: グーグルのエンジニアが警告、「別タブで開く」リンクは実はヤバいんだって!?
【参考】ここまでのコミット内容
https://dev.azure.com/nightonly/rails-app-origin/_git/rails-app-origin/commit/e6db5bd50dbbbebce41cc6458b5b10549d40fbf0
