はじめに
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 11章 11.2 アカウント有効化のメール送信の演習まとめ&解答例です。
個人の解答例なので、誤りがあればご指摘ください。
動作環境
- cloud9
- ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
- Rails 5.0.0.1
11.2.1 送信メールのテンプレート
本章での学び
アカウント有効化メールを送信するために、Action Mailerライブラリを使用して、メイラーを作成する。
【mailer】Userメイラーの作成
rails generate
で自動生成する。
viewのテンプレートが2つ生成される。
- テキストメール用
- HTMLメール用
yokoyan:~/workspace/sample_app (account-activation) $ rails generate mailer UserMailer account_activation password_reset Running via Spring preloader in process 1731 Expected string default value for '--jbuilder'; got true (boolean) create app/mailers/user_mailer.rb invoke erb create app/views/user_mailer create app/views/user_mailer/account_activation.text.erb create app/views/user_mailer/account_activation.html.erb create app/views/user_mailer/password_reset.text.erb create app/views/user_mailer/password_reset.html.erb invoke test_unit create test/mailers/user_mailer_test.rb create test/mailers/previews/user_mailer_preview.rb
【mailer】メールテンプレートのカスタマイズ
デフォルトのfromアドレスを編集する。 なお、この値はアプリケーション全体で共通となる。
default from: 'noreply@example.com'
【mailer】メール送信処理の追加
自動生成された、account_activationメソッドを編集する。
ユーザー情報のインスタンス変数を作成し、user.email
宛にメールを送信する。
def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end
【view】テキストメールとHTMLメールビューのカスタマイズ
ユーザーの有効化URLには、有効化トークンを含める。
q5lt38hQDc_959PVoo6b7A
の文字列は、実装済みのnew_token
メソッドで生成した値。
Base64でエンコードされている。
なお、AccountActivationsコントローラのeditアクションでは、paramsハッシュでparams[:id]
として参照することができる。
http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit
また、URLからユーザーを特定するために、メールアドレスをクエリパラメータで追加する。 URLの末尾に?を付与して、キーと値のペアを記載する。
なお、URLに記載するメールアドレスの@は、URLで使えない文字列であるため
%40
でエスケープする必要がある。
Railsでは自動的にエスケープしてくれる。
account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
上記を踏まえ、実装する。
テキストメールのテンプレート。
Hi <%= @user.name %> Welcome to the Sample App! Click on the link below to active your account: <%= edit_account_activation_url(@user.activation_token, email: @user.email) %>
HTMLメールのテンプレート。
<h1>Sample App</h1> <p>Hi <%= @user.name %></p>, <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %>
テストコードの作成
演習1
コンソールを開き、CGIモジュールのescapeメソッド (リスト 11.15) でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don’t panic!“をエスケープすると、どんな結果になりますか?
実行結果は以下の通り。 @や、'、!がエスケープされて表示される。
yokoyan:~/workspace/sample_app (account-activation) $ rails console Running via Spring preloader in process 1755 Loading development environment (Rails 5.0.0.1) >> CGI.escape('foo@example.com') => "foo%40example.com" >> >> CGI.escape("Don't panic!") => "Don%27t+panic%21" >>
11.2.2 送信メールのプレビュー
本章での学び
特殊なURLにアクセスして、メールの内容をその場でプレビューする。
【environment】develop環境のメール設定変更
ホスト名を各自の環境に合わせて設定する。
config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :test host = 'xxxxxxxxxxxx.c9users.io' config.action_mailer.default_url_options = { host: host, protocol: 'https' }
設定が完了したら、develop環境のサーバを再起動する。
【mailer】Userメイラーのプレビューファイルの修正
自動生成されたプレビューファイルは、そのままでは動かないため、ユーザー情報を追加する。
- DBの最初のユーザー
- 有効化トークン(メールテンプレート内で使用しているため省略不可)
def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end
動作確認
ブラウザから、下記URLにアクセスする。
https://xxxxxxxxx.c9users.io/rails/mailers/user_mailer/account_activation.html
メイラーのプレビュー画面が表示される。
演習1
Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?
上記の動作確認の通り。 「Date」には、UTC時間が表示されているため、日本と9時間ずれてる。
11.2.3 送信メールのテスト
本章での学び
前項で実装した、メールプレビューのテストコードを作成する。
【test】Userメイラーのテストコードの作成
- テストユーザーとしてfixtureからmichaelを取得する
- テストユーザーの有効化トークンを生成する
- Userメイラーのアカウント有効化処理にテストユーザー情報を渡して、メールオブジェクトを作成する
- メールオブジェクトの件名をチェックする
- メールオブジェクトの送信先メールアドレスをチェックする
- メールオブジェクトの送信元メールアドレスをチェックする
- テストユーザーの名前が、メールの本文に含まれているかチェックする
- テストユーザーの有効化トークンが、メールの本文に含まれているかチェックする
- テストユーザーのアドレスがエスケープされて、メールの本文に含まれているかチェックする
上記を踏まえ実装する。
自動的に生成されるtest "password_reset"
については、12章で実装するためコメントアウトする。
(残しておくとテストがredになってしまう)
class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI.escape(user.email), mail.body.encoded end
【config】テストファイル内のドメイン名を設定する
テスト内でのドメインを設定する。
config.action_mailer.default_url_options = { host: 'example.com' }
動作確認
mailerのテストがgreenになることを確認。
yokoyan:~/workspace/sample_app (account-activation) $ rails test:mailers Started with run options --seed 17823 1/1: [========================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.63635s 1 tests, 9 assertions, 0 failures, 0 errors, 0 skips
演習1
この時点で、テストスイートが greenになっていることを確認してみましょう。
テストスイートがgreenであることを確認。
yokoyan:~/workspace/sample_app (account-activation) $ rails test Running via Spring preloader in process 2015 Started with run options --seed 24199 43/43: [======================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.04996s 43 tests, 188 assertions, 0 failures, 0 errors, 0 skips
演習2
リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが redに変わることを確認してみましょう。
CGI.escape
部分をコメントアウトする。
# assert_match CGI.escape(user.email), mail.body.encoded assert_match user.email, mail.body.encoded
テストスイートがredになることを確認。
yokoyan:~/workspace/sample_app (account-activation) $ rails test Running via Spring preloader in process 2134 Started with run options --seed 58681 FAIL["test_account_activation", UserMailerTest, 1.9940800210024463] test_account_activation#UserMailerTest (1.99s) Expected /michael@example\.com/ to match # encoding: US-ASCII "\r\n----==_mimepart_594c3c6d68deb_85612410e8528f0\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHi Michael Example\r\n\r\nWelcome to the Sample App! Click on the link below to active your account:\r\n\r\nhttp://example.com/account_activations/pFlo06HSkw3Yug79-uZaQg/edit?email=michael%40example.com\r\n\r\n\r\n\r\n----==_mimepart_594c3c6d68deb_85612410e8528f0\r\nContent-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<!DOCTYPE html>\r\n<html>\r\n <head>\r\n <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n <style>\r\n /* Email styles need to be inline */\r\n </style>\r\n </head>\r\n\r\n <body>\r\n <h1>Sample App</h1>\r\n\r\n<p>Hi Michael Example</p>,\r\n\r\n<p>\r\n Welcome to the Sample App! Click on the link below to activate your account:\r\n</p>\r\n\r\n<a href=\"http://example.com/account_activations/pFlo06HSkw3Yug79-uZaQg/edit?email=michael%40example.com\">Activate</a>\r\n\r\n </body>\r\n</html>\r\n\r\n----==_mimepart_594c3c6d68deb_85612410e8528f0--\r\n". test/mailers/user_mailer_test.rb:14:in `block in <class:UserMailerTest>' 43/43: [======================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.15642s 43 tests, 188 assertions, 1 failures, 0 errors, 0 skips
動作確認後は、コメントアウト部分を元に戻しておく。
11.2.4 ユーザーのcreateアクションを更新
本章での学び
作成したメイラーを使うために、createアクションを修正する。
【controller】ユーザー登録時に、アカウント有効化メールの送信処理を追加する
以下の処理を、
if @user.save log_in(@user) #保存の成功をここで扱う flash[:success] = "Welcome to the Sample App!" redirect_to user_url @user else
以下のように修正する。
deliver_now
は、メールを送信するメソッド。
アカウント有効化メールを送信後、ログインするのではなく、TOPページにリダイレクトさせる
if @user.save UserMailer.account_activation(@user).deliver_now flash[:success] ="Please check your email to activate your account" redirect_to root_url else
【test】統合テストの修正
現時点では、テストがredになってしまう。
yokoyan:~/workspace/sample_app (account-activation) $ rails test Running via Spring preloader in process 1859 Started with run options --seed 60543 FAIL["test_valid_signup_information", UsersSignupTest, 1.6497548810002627] test_valid_signup_information#UsersSignupTest (1.65s) expecting <"users/show"> but rendering with <["user_mailer/account_activation", "layouts/mailer", "static_pages/home", "layouts/_rails_default", "layouts/_shim", "layouts/_header", "layouts/_footer", "layouts/application"]> test/integration/users_signup_test.rb:37:in `block in <class:UsersSignupTest>' 43/43: [========================================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.49370s 43 tests, 186 assertions, 1 failures, 0 errors, 0 skips
失敗するテストを一時的にコメントアウトする。
test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! # assert_template 'users/show' # assert_not flash.empty? # assert is_logged_in? end
テストがgreenになることを確認。
yokoyan:~/workspace/sample_app (account-activation) $ rails test Running via Spring preloader in process 1971 Started with run options --seed 16030 43/43: [========================================================================================================================================================================================================] 100% Time: 00:00:02, Time: 00:00:02 Finished in 2.11845s 43 tests, 185 assertions, 0 failures, 0 errors, 0 skips
動作確認
ブラウザから、ユーザー登録を行う。 (test2ユーザーで登録を実施)
コンソールにメール送信ログが表示されていることを確認。
----==_mimepart_594c956b2e8ac_82a276bab859751 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi test2 Welcome to the Sample App! Click on the link below to active your account: https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com ----==_mimepart_594c956b2e8ac_82a276bab859751 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style> /* Email styles need to be inline */ </style> </head> <body> <h1>Sample App</h1> <p>Hi test2</p>, <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com">Activate</a> </body> </html>
演習1
新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?
コンソールログをより確認。 リダイレクト先のURLが下記の通りルートURLに302で送信されていることを確認。
Redirected to https://rails-tutorial-yokoyan.c9users.io/ Completed 302 Found in 747ms (ActiveRecord: 21.9ms)
有効化トークンの値は、authenticity_token"=>"BChjYdix9aSf0+wv9gPXrz3WoPJzZPKLVPE/bSqMMskkQkQ0cwXPbKmyr583JHM+0APDwxH37qT6+YbHNKWxwg==
Started GET "/" for 125.199.218.217 at 2017-06-23 04:13:13 +0000 Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 ActiveRecord::SchemaMigration Load (0.3ms) SELECT "schema_migrations".* FROM "schema_migrations" Processing by StaticPagesController#home as HTML Rendering static_pages/home.html.erb within layouts/application Rendered static_pages/home.html.erb within layouts/application (320.5ms) Rendered layouts/_rails_default.html.erb (100.5ms) Rendered layouts/_shim.html.erb (0.4ms) Rendered layouts/_header.html.erb (3.9ms) Rendered layouts/_footer.html.erb (0.4ms) Completed 200 OK in 447ms (Views: 436.8ms | ActiveRecord: 0.0ms) Started GET "/signup" for 125.199.218.217 at 2017-06-23 04:13:17 +0000 Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by UsersController#new as HTML Rendering users/new.html.erb within layouts/application Rendered shared/_error_messages.html.erb (0.4ms) Rendered users/_form.html.erb (421.1ms) Rendered users/new.html.erb within layouts/application (422.4ms) Rendered layouts/_rails_default.html.erb (30.7ms) Rendered layouts/_shim.html.erb (0.4ms) Rendered layouts/_header.html.erb (0.7ms) Rendered layouts/_footer.html.erb (0.5ms) Completed 200 OK in 473ms (Views: 460.4ms | ActiveRecord: 0.7ms) Started POST "/signup" for 125.199.218.217 at 2017-06-23 04:13:30 +0000 Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 ActiveRecord::SchemaMigration Load (0.1ms) SELECT "schema_migrations".* FROM "schema_migrations" Processing by UsersController#create as HTML Parameters: {"utf8"=>"✓", "authenticity_token"=>"BChjYdix9aSf0+wv9gPXrz3WoPJzZPKLVPE/bSqMMskkQkQ0cwXPbKmyr583JHM+0APDwxH37qT6+YbHNKWxwg==", "user"=>{"name"=>"test2", "email"=>"test2@example.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"}, "commit"=>"Create my account"} (0.1ms) begin transaction User Exists (5.9ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "test2@example.com"], ["LIMIT", 1]] SQL (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?) [["name", "test2"], ["email", "test2@example.com"], ["created_at", 2017-06-23 04:13:30 UTC], ["updated_at", 2017-06-23 04:13:30 UTC], ["password_digest", "$2a$10$ox4DtTzrIjUOsCTFUe3dg.aqbZXJ1taP0HF.v6cTr6C4DUs3zImTK"], ["activation_digest", "$2a$10$OhYHKhyL8LqTJ/r2livKRuRTVhWSfXROncbmcmHmmWVmpINb9xE.S"]] (14.9ms) commit transaction Rendering user_mailer/account_activation.html.erb within layouts/mailer Rendered user_mailer/account_activation.html.erb within layouts/mailer (6.1ms) Rendering user_mailer/account_activation.text.erb within layouts/mailer Rendered user_mailer/account_activation.text.erb within layouts/mailer (0.4ms) UserMailer#account_activation: processed outbound mail in 150.2ms Sent mail to test2@example.com (8.9ms) Date: Fri, 23 Jun 2017 04:13:31 +0000 From: noreply@example.com To: test2@example.com Message-ID: <594c956b30451_82a276bab859864@yokoyan-rails-tutorial-4550834.mail> Subject: Account activation Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_594c956b2e8ac_82a276bab859751"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_594c956b2e8ac_82a276bab859751 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi test2 Welcome to the Sample App! Click on the link below to active your account: https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com ----==_mimepart_594c956b2e8ac_82a276bab859751 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style> /* Email styles need to be inline */ </style> </head> <body> <h1>Sample App</h1> <p>Hi test2</p>, <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="https://rails-tutorial-yokoyan.c9users.io/account_activations/oa_jXO-rDZ4i_xBqG2knXg/edit?email=test2%40example.com">Activate</a> </body> </html> ----==_mimepart_594c956b2e8ac_82a276bab859751-- Redirected to https://rails-tutorial-yokoyan.c9users.io/ Completed 302 Found in 747ms (ActiveRecord: 21.9ms) Started GET "/" for 125.199.218.217 at 2017-06-23 04:13:31 +0000 Cannot render console from 125.199.218.217! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 Processing by StaticPagesController#home as HTML Rendering static_pages/home.html.erb within layouts/application Rendered static_pages/home.html.erb within layouts/application (165.7ms) Rendered layouts/_rails_default.html.erb (50.5ms) Rendered layouts/_shim.html.erb (0.4ms) Rendered layouts/_header.html.erb (3.6ms) Rendered layouts/_footer.html.erb (0.4ms) Completed 200 OK in 229ms (Views: 227.6ms | ActiveRecord: 0.0ms)
演習2
コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。
コンソールから確認。 追加されたユーザーの有効化ステータスが、falseであることを確認。
yokoyan:~/workspace/sample_app (account-activation) $ rails console Running via Spring preloader in process 2627 Loading development environment (Rails 5.0.0.1) >> test2 = User.find_by(email: "test2@example.com") User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "test2@example.com"], ["LIMIT", 1]] => #<User id: 101, name: "test2", email: "test2@example.com", created_at: "2017-06-23 04:13:30", updated_at: "2017-06-23 04:13:30", password_digest: "$2a$10$ox4DtTzrIjUOsCTFUe3dg.aqbZXJ1taP0HF.v6cTr6C...", remember_digest: nil, admin: false, activation_digest: "$2a$10$OhYHKhyL8LqTJ/r2livKRuRTVhWSfXROncbmcmHmmWV...", activated: nil, activated_at: nil> >> ?> test2.activated? => false >>
おわりに
メールの送信処理の組み込みが完了しました。 メールテンプレートの作成や、メール送信処理そのものも、 Railsだと簡単に実装できます。
有効化トークンを埋め込んだURLによる認証は、ECサイト等でも当たり前にある機能であるため、 仕組みが非常に勉強になりました。