Rails 「Action Mailer × Gmail × Heroku」でメール送信機能を実装する
Railsアプリケーションでメール送信機能を実装する方法を説明します。
Herokuの本番環境では、SendGridを用いることが多いと思いますが、今回は開発/本番環境どちらもGmailを使って実装します。
なお、HerokuとGoogleのアカウント取得、およびHerokuでのアプリケーションセットアップが完了している前提で進めます。
- 環境
- ステップ 1: 環境変数の設定
- ステップ 2: Action Mailerの設定
- ステップ 3: メールビューの作成
- ステップ 4: 開発環境の設定
- ステップ 5: 本番環境の設定
- ステップ 6: メール送信のトリガーを設定
- ステップ 7: テストとデプロイ
環境
ステップ 1: 環境変数の設定
1.1: Googleでアプリパスワードを取得
以下の順にアクセスして、アプリパスワードを取得します。
1.2: gem dotenv-rails をインストール
セキュリティを確保するために、Gmailのユーザー名とパスワードは環境変数に保存します。
Railsでアプリケーションでgem 'dotenv-rails'
をGemfileに追加し、bundle install
を実行して、dotenvをアプリケーションにインストールします。
1.3: .envファイルにメールアドレスとパスワードを設定
以下のように.env
ファイルに環境変数を追加しましょう。
EMAIL_USER=your-email@gmail.com EMAIL_PASSWORD=your-email-password
your-email-password
には、先ほどGoogleアカウントで作成したアプリパスワードを記述します。
ステップ 2: Action Mailerの設定
今回は、ユーザーが商品を購入したら、購入明細をメールで送信するようにします。
app/mailers/checkout_mailer.rb
にメーラークラスを作成し、購入確認メールを定義します。
class CheckoutMailer < ApplicationMailer default from: ENV['EMAIL_USER'] def checkout_confirmation(order) @order = order mail(to: @order.billing_address.email, subject: '購入が完了しました') end end
メールの送信元として、先ほど.envファイルに記述した環境変数を設定します。
checkout_confirmation(order)
では、引数で購入内容の情報を取得します。
インスタンス変数@order
にすることで、メールの本文で使用できるようになります。
mail(to: )には購入内容の情報から、購入者のメールアドレスを設定します。
ステップ 3: メールビューの作成
HTMLとテキスト形式の両方でメールを作成します。
これはapp/views/checkout_mailer
ディレクトリに配置します。
HTML
<!DOCTYPE html> <html> <head> <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' /> </head> <body> <h1>購入が完了しました</h1> <p>購入明細:</p> <ul> <% @order.order_items.each do |item| %> <li><%= item.product.name %> - <%= item.quantity %> x <%= number_to_currency(item.price) %></li> <% end %> </ul> <p>Total: <%= number_to_currency(@order.total_price) %></p> </body> </html>
テキスト形式
購入が完了しました 購入明細: <% @order.order_items.each do |item| %> <%= item.product.name %> - <%= item.quantity %> x <%= number_to_currency(item.price) %> <% end %> Total: <%= number_to_currency(@order.total_price) %>
ステップ 4: 開発環境の設定
開発環境では、config/environments/development.rb
にメール送信の設定を追加します。
config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: 'smtp.gmail.com', port: 587, domain: 'gmail.com', user_name: ENV['EMAIL_USER'], password: ENV['EMAIL_PASSWORD'], authentication: 'plain', enable_starttls_auto: true }
ステップ 5: 本番環境の設定
本番環境でHerokuを使用する場合、config/environments/production.rb
に以下のように設定を追加します。
config.action_mailer.default_url_options = { host: 'https://your-app-name.herokuapp.com', protocol: 'https' } config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: 'smtp.gmail.com', port: 587, domain: 'gmail.com', user_name: ENV['EMAIL_USER'], password: ENV['EMAIL_PASSWORD'], authentication: :plain, enable_starttls_auto: true }
また、Herokuの管理画面(Setting > Config Vars)にて環境変数(EMAIL_USER, EMAIL_PASSWORD)を追加します。
ステップ 6: メール送信のトリガーを設定
今回は、購入が完了したタイミングでメールを送信します。
OrdersController
内で、注文が正常に保存された後にメールを送信するようにします。
if @order.save CheckoutMailer.checkout_confirmation(@order).deliver_now # その他の処理... end
先ほど作成したcheckout_confirmation
アクションに、保存したorder
を引数として渡すことで、購入内容をメールで使用できます。
ステップ 7: テストとデプロイ
開発環境でメールが正しく送信されることを確認したら、本番環境にデプロイします。
【Rails】flash[:notice]とか書くから、flashのキーはシンボルだと思ってたら「文字列」だった
Railsでの開発中に遭遇した不具合について、原因と解決策を投稿します。
環境
背景
flashを使って以下のようにメッセージを表示していました。
<% if flash[:notice] %> <div class="alert alert-primary"> <%= flash[:notice] %> </div> <% end %>
flash[:notice]だけではなく、flash[:alert]を受け取った場合もメッセージを表示します。
<% if flash[:notice] %> <div class="alert alert-primary"> <%= flash[:notice] %> </div> <% end %> <% if flash[:alert] %> <div class="alert alert-danger"> <%= flash[:alert] %> </div> <% end %>
しかし、上記のように種類が増えるにつれてif文を増やしていくのは、少し冗長的なので、case文を使ってもう少しスッキリさせてみます。
<% flash.each do |type, message| %> <% case type when :notice %> <div class="alert alert-primary"> <%= message %> </div> <% when :error %> <div class="alert alert-danger"> <%= message %> </div> <% end %> <% end %>
不具合の内容
既述の case文を使った書き方にすると、なぜかflashが画面に表示されなくなってしまいました。
不具合の原因
flashはflash[:notice] = "message"
のように指定するため、flashのキーはシンボルかと思っていました。
しかし、実際に確認してみると、キーは「文字列」になっていることがわかります。
{"alert"=>"An error occurred: Quantity must be greater than 0"}
そのため、シンボルで指定すると条件に一致しません。
これはHashWithIndifferentAccess
というRailsの機能で、シンボルと文字列の両方をキーとして受け取れるが、内部的にはそれを「文字列」として保存する仕様によるものだと思います。
解決策
この問題を解決するためには、case文でflashのキーを指定する際に「文字列」で指定する必要があります。
<% flash.each do |key, message| %> <% case key when "notice" %> <div class="alert alert-primary"> <%= message %> </div> <% when "alert" %> <div class="alert alert-danger"> <%= message %> </div> <% end %> <% end %>
なお、このcase文はヘルパーメソッドとして、外部で定義した方がよりビューが綺麗になりそうです。
# app/helpers/application_helper.rb def flash_class(type) case type when "notice" then "alert alert-primary" when "alert" then "alert alert-danger" # 他のflashタイプもここに追加できます end end
<% flash.each do |type, message| %> <div class="<%= flash_class(type) %>"> <%= message %> </div> <% end %>
【Rails7】.to_iメソッドと||演算子でカート内の商品数が0になる不具合
Railsでセッションベースのカート機能を実装する際に遭遇した不具合について、原因と解決策を投稿します。
環境
背景
アプリケーション内にて、商品一覧ページから「add to cart」ボタンを押すと、該当の商品を「1つ」カートに追加する機能の実施を試みました。
def add_item item_id = params[:item_id] quantity = params[:quantity].to_i || 1
商品詳細ページからは任意の数量を指定してカートに追加できる仕様にするため、以下のようにquantity
に数値を入れます。
- paramsで
quantity
が渡ってくる -> その数値を入れる - paramsで
quantity
が渡ってこない -> 1を入れる
なお、quantity
を数値として管理したいため、.to_i
メソッドを使用しています。
不具合の内容
カート内の商品数量がすべて0に設定されてしまうという問題が発生しました。
不具合の原因
.to_i
メソッドの仕様によるものでした。
商品一覧ページからカートに商品を追加した場合はparams[:quantity]
がnil
になるため、1
がquantityに入れられるはずです。
しかし.to_i
メソッドは、params[:quantity]
がnil
か空文字の場合、これを0
と解釈してしまうようです。
そのため、
quantity = params[:quantity].to_i || 1
||
演算子の左側が真になる(0になる)ので、カート内の商品数が0になってしまっていたようです。
解決策
この問題を解決するためには、
まず.present?
メソッドでparams[:quantity]
の存在を確認し、その後適切に値を設定する必要があります。
三項演算子を使う
quantity = params[:quantity].present? ? params[:quantity].to_i : 1
if文を使う
if params[:quantity].present? quantity = params[:quantity].to_i else quantity = 1 end
【Rails7】 名前空間付きコントローラにおけるlink_to 削除でRoutingErrorになった
管理者向けの商品削除機能を実施するために、名前空間付きコントローラー(今回のケースではadmin/products
)でlink_to
メソッドを使用しました。
この際、名前空間の存在を意識せずに実装するとRoutingError
になったので、原因と解消方法を投稿します。
目次
環境
背景
ディレクトリ構造:
app/ |-- controllers/ | |-- admin/ | | `-- products_controller.rb | `-- products_controller.rb |-- views/ | |-- admin/ | | `-- products/ | | `-- index.html.erb | `-- products/ | `-- index.html.erb ...
ルーティング:
namespace :admin do resources :products end resources :products, only: %i[index show]
コントローラ:
# app/controllers/products_controller.rb class ProductsController < ApplicationController def index @products = Product.all end def show @product = Product.find(params[:id]) end end # app/controllers/admin/products_controller.rb class Admin::ProductsController < ApplicationController before_action :set_product, only: %i[ edit update destroy ] # 中略 # def destroy @product.destroy redirect_to admin_products_path, flash: { notice: "#{@product.name}を削除しました"} end private def set_product @product = Product.find_by(id: params[:id]) end end
一般ユーザー向けにはdomain/products
で商品一覧ページを表示させ、管理者向けにdomain/admin/products
で管理画面内の商品一覧ページを表示させ、そこで削除機能を実装します。
エラーの内容
今回、管理画面内の商品一覧ページに下記のような削除リンクを設けました。
app/views/admin/products/index.html.erb
<% @products.each do |product| %> ~~ <%= link_to product, data: { turbo_method: :delete, turbo_confirm: '削除してもよろしいですか?'} %> ~~ <% end %>
link_to の引数に、インスタンスを渡し、turbo_method: :delete
を指定することで、destroy
アクションの実行を期待しました。
しかし、以下のようなエラーが出てしまいました。
ActionController::RoutingError (No route matches [DELETE] "/products/[:id]"):
ルーティングも設定しているのに、ルーティングエラーが発生とは・・・。
エラーの原因
エラーの内容をよく見ると[DELETE] "/products/[:id]")
ということで、admin/products/[:id]
ではなく、一般ユーザー向けに作成したproductsのパスになっていました。
そもそも、link_to
メソッドの引数にモデルオブジェクト(インスタンス)を渡すと、そのオブジェクトのshow
アクションにマッピングされるURL(例えば /products/1
)が生成されます。
そのため、link_to
メソッドでは引数にデルオブジェクト(インスタンス)を渡し、turbo_method: :delete
を指定するだけで、特定の商品を削除できるはずでした。
しかし、上記のコードでは、名前空間を考慮せずに標準の商品コントローラーへのパスを構築してしまうため、admin
配下の商品コントローラにルーティングされるよう明示的に記述する必要があったようです。
link_to
があまりにもスマートな機能だからといって、考えなしにインスタンスを指定するのはよくありませんでした。。。
解決策
この問題を回避するためには、link_to
メソッドに明示的にパスを指定する必要があります。
具体的には、以下の2通りがあります
# 名前空間を明示的に指定したパスヘルパー <%= link_to admin_product_path(product), data: { turbo_method: :delete, turbo_confirm: '削除してもよろしいですか?'} %>
# ポリモーフィックURLヘルパー <%= link_to [ :admin, product ], data: { turbo_method: :delete, turbo_confirm: '削除してもよろしいですか?'} %>
参考:https://runebook.dev/ja/docs/rails/actiondispatch/routing/polymorphicroutes
いずれにしても、admin
配下のproductsコントローラを使うことを明示的に指定しないといけません。
【Ruby / Rails】URI.openを使うと、RuboCopで警告が出た
Railsで開発をしている際に、外部から画像データを参照するためにopen-uri
を使用しました。
require 'open-uri' image_url = "https://example.com/image.jpg" image_data = URI.open(image_url).read
しかし、RuboCopで警告が出たので、最終的にNet::HTTP
に切り替えました。
1. RuboCopの警告内容
警告内容は以下の通りです。
C: Security/Open: The use of URI.open is a serious security risk.
この警告は、URI.open
の使用が重大なセキュリティリスクを持つ可能性があるため、注意が必要であることを示しています。
2. なぜURI.open
はリスクがあるのか?
open-uri
は非常に便利なライブラリですが、特に外部からの入力をそのままURI.open
に渡すと、意図しないファイルやリソースが開かれる可能性があります。
これにより、悪意のある攻撃者によってサーバが危険にさらされる可能性があります。
参考:https://qiita.com/jabba/items/7e018139fff586b4a06d
3. URI.parse#open
を使用する
ドキュメントを確認すると、URI.parse#open
を使用すれば警告は解除されるようでした。
require 'open-uri' image_url = "https://example.com/image.jpg" image_data = URI.parse(image_url).open.read
これで実際に警告は解除されました。
4. Net::HTTP
を使用する
open-uri
ではなく、Net::HTTP
を使用してデータを取得します。
元のopen-uri
を使用したコード:
require 'open-uri' image_url = "https://example.com/image.jpg" image_data = URI.open(image_url).read
Net::HTTP
を使用したコードへの変更:
require 'net/http' require 'uri' image_url = "https://example.com/image.jpg" url = URI.parse(image_url) response = Net::HTTP.get_response(url) if response.is_a?(Net::HTTPSuccess) image_data = response.body else puts "Failed to download image: #{response.message}" end
上記の変更により、URLからのデータのダウンロードはNet::HTTP
を使用して行われるようになります。
4. まとめ
Rails開発において、外部の画像データを取得する際にopen-uri
を使用すると便利ですが、URI.open
の使用にはセキュリティ上のリスクがあるとの警告がRuboCopから示されることがあります。
この警告は、外部からの入力をそのまま使用すると、意図しないリソースのアクセスが生じる可能性があるためです。
安全な取得のための代替策として、URI.parse#open
かNet::HTTP
がありました。
『現場で使える Ruby on Rails 5 速習実践ガイド』感想ブログ
『現場で使える Ruby on Rails 5 速習実践ガイド』という書籍(以下、本書)に目を通しましたので、その感想を共有させていただきます。
結論
本書は、Ruby on Railsを学びたいと考えている初心者や、基礎を見直したい中級者にとって、非常に有益な内容が詰まっている一冊と言えます。
Rubyの基礎から、RailsのMVC構造、CRUD操作、更には発展的な実装方法まで、幅広く学ぶことができます。
内容が難しくなる部分もありますが、実際の開発現場での知識やチーム開発のコツなど、実践的な情報も得られると感じました。
良かったところ
Rubyの基礎解説
Rubyの超基本的なところから学ぶことができます。Rubyが全く未経験の方には、少し解説が少ないかもしれませんが、復習には最適なボリュームです。わかりやすいデザインと図解
デザインが読みやすく、図解も多く用意されているため、視覚的にも情報を取り込みやすいです。MVCの詳細解説
Railsの特徴的なMVC構造について、非常に分かりやすく説明されています。実際にタスク管理ツールを作成しながら学べるのもポイント高いです。実践的な補足情報
これはどういうこと?と思う場面での注釈や、チーム開発の際のエッセンスなど、実務で役立つ情報が満載です。
学んだこと
- Rubyの基礎:オブジェクトの概念や、クラス、モジュールなど
- Railsの環境構築からMVCの基礎
- CRUD操作の実装方法
- 発展的な機能の実装:検索やソート、ページネーションなど
- AjaxやTurbolinksの活用方法
- チームでの開発やGitHubの利用方法
- アップデート時の注意点や方法
難しかったこと
- 本書の中で、複数人での開発の際のポイントが詳しく解説されていますが、実際に経験していないと理解が難しい部分も確かにありました。
- また、発展的な内容が増える後半部分は、特に実装経験がないと想像しにくいかもしれません。
まとめ
『現場で使える Ruby on Rails 5 速習実践ガイド』は、内容が豊富で、実践的な情報も満載なので、これからRailsの開発を行なっていく上で参考にできる1冊だと感じました。
【Rails】newアクションでオブジェクトを作ったのに、なぜcreateアクションでも再度オブジェクトを作るのか
Railsの学習をしていて、newアクションで新規登録用のオブジェクトを作ったんだから、それをそのままcreateアクションで使ってくれよと思ったことがありました。
def create @user = User.new(user_params) if @user.save # 保存成功時の処理 else # 保存失敗時の処理 end end def new @user = User.new end
かなり基本的なことではありますが、newアクションでオブジェクトを作ったのに、なぜcreateアクションでも再度オブジェクトを作るのかを改めて調べてみました。
結果:Webアプリのステートレスな性質によるもの
Webアプリケーションは基本的に「ステートレス」です。
つまり、各リクエストは独立しており、1つのリクエストで作成された変数やオブジェクトは、次のリクエストで自動的に保持されません。
newアクションとcreateアクション
例えば、以下のようなnew
アクションがあるとします。
def new @user = User.new end
new
アクションは、ユーザーにデータ入力のためのフォームを表示する役割があります。
ここで生成された@user
オブジェクトは、ビューでのフォーム表示のためだけに使われます。
# new.html.erb <%= form_for @user do |f| %> <!-- フィールドの入力 --> <%= f.submit '登録' %> <% end %>
一方、create
アクションは、このフォームから送信されたデータを受け取って、データベースに保存する役割があります。
ここで新しいUserオブジェクトを生成する理由は、前述したステートレスな性質によるものです。
new
アクションで生成された@user
はこのcreate
アクションでは利用できないため、新たにオブジェクトを生成して、フォームから送信されたデータを使ってデータベースに保存します。
def create @user = User.new(user_params) if @user.save # 保存成功時の処理 else # 保存失敗時の処理 end end
まとめ
Railsのnew
アクションとcreate
アクションは、Webアプリケーションがステートレスなため、それぞれ異なるリクエストとして独立して動作します。
そのため、create
アクションで新たにオブジェクトを生成することで、データの保存を実現しています。