久々にメール送信の実装を行ったので、メモしておきます。
難しくはないけど、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