【Rails】お問い合わせフォーム(確認画面付き)の実装手順【完全版】

はじめに

お問い合わせフォームの実装手順をいつも忘れてしまうので、完全な実装手順をまとめます。

メーラー作成

以下のコマンドを実行し、メーラーを作成します。
$ rails g mailer ContactMailer
作成されたメーラー「app/mailers/contact_mailer.rb」を編集します。
class ContactMailer < ApplicationMailer
  default from: 'noreply@example.com'
  default to: 'admin@example.com'
  layout 'mailer'

  def send_mail(contact)
    @contact = contact
    mail(from: contact.email, to: ENV['MAIL_ADDRESS'], subject: 'Webサイトより問い合わせが届きました') do |format|
      format.text
    end
  end
end
メール本文になるテンプレートファイル「app/views/contact_mailer/send_mail.text.erb」を編集します。
<%= @contact.content %>

<%= @contact.name %> (<%= @contact.email %>)

コントローラー作成

以下のコマンドを実行し、コントローラーを作成します。
$ rails g controller Contacts
作成されたコントローラー「app/controllers/contacts_controller.rb」を編集します。
class ContactsController < ApplicationController
  def index
      @contact = Contact.new
    end
   
    def confirm
      @contact = Contact.new(contact_params)
      if @contact.valid?
        render :action => 'confirm'
      else
        render :action => 'index'
      end
    end

    def done
      @contact = Contact.new(contact_params)
      if params[:back]
        render :action => 'index'
      else
        ContactMailer.send_mail(@contact).deliver_now
        render :action => 'done'
      end
    end

    private
      def contact_params
        params.require(:contact).permit(:name, :email, :content)
      end
end

モデル作成

メールを送信するだけならDBは必要ないので、ActiveModelを使います。
「app/models/contact.rb」を手動作成(rails generateは使わない)し、編集します。
class Contact include ActiveModel::Model
    attr_accessor :name, :email, :content
   
    validates :name, presence: true, length: { maximum: 20 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    validates :email, presence: true, length: { maximum: 30 },
                      format: { with: VALID_EMAIL_REGEX }
    validates :content, presence: true, length: { maximum: 500 }
end

ビュー作成

以下の3ファイルを作成します。

app/views/contacts/index.html.erb
<% provide :title, 'お問い合わせ' %>
<div class="container mt-4">
  <div class="row">
    <div class="col-md-12 col-lg-6 offset-lg-3">
      <h2><%= icon('fas', 'envelope') %> お問い合わせ</h2>
      <nav aria-label="パンくずリスト" class="small">
        <ol class="breadcrumb">
          <li class="breadcrumb-item"><%= link_to 'トップページ', root_path %></li>
          <li class="breadcrumb-item active" aria-current="page">お問い合わせ</li>
        </ol>
      </nav>
      <hr />
      
      <%= form_with model: @contact, local: true, url: contact_confirm_path do |form| %>
        <% if @contact.errors.any? %>
          <div class="alert alert-danger" role="alert">
            <h5><strong>入力内容にエラーがあります。</strong></h5>
            <ul>
              <% @contact.errors.full_messages.each do |message| %>
                <li><%= message %></li>
              <% end %>
            </ul>
          </div>
        <% end %>

        <div class="form-group">
          <%= form.label :name, '名前' %>
          <span class="badge badge-info rounded-0">必須</span>
          <%= form.text_field :name, id: 'name', class: 'form-control', :placeholder => '名前' %>
        </div>

        <div class="form-group">
          <%= form.label :email, 'メールアドレス' %>
          <span class="badge badge-info rounded-0">必須</span>
          <small class="text-muted">例:user@example.com</small>
          <%= form.text_field :email, id: 'email', class: 'form-control', :placeholder => 'メールアドレス' %>
        </div>

        <div class="form-group">
          <%= form.label :content, 'お問い合わせ内容' %>
          <span class="badge badge-info rounded-0">必須</span>
          <small class="text-muted">パスワードなどの機密情報を含めないでください。</small>
          <%= form.text_area :content, id: 'content', class: 'form-control', :placeholder => 'お問い合わせ内容', :rows => '5' %>
        </div>

        <div class="float-right">
          <%= form.submit "確認", class: "btn btn-dark" %>
        </div>
      <% end %>

    </div>
  </div>
</div>

app/views/confirm.html.erb
<% provide :title, 'お問い合わせ' %>
<div class="container mt-4">
  <div class="row">
    <div class="col-md-12 col-lg-6 offset-lg-3">
      <h2><%= icon('fas', 'envelope') %> お問い合わせ</h2>
      <nav aria-label="パンくずリスト" class="small">
        <ol class="breadcrumb">
          <li class="breadcrumb-item"><%= link_to 'トップページ', root_path %></li>
          <li class="breadcrumb-item active" aria-current="page">お問い合わせ</li>
        </ol>
      </nav>
      <hr />

      <%= form_with model: @contact, local: true, url: contact_done_path do |form| %>

        <div class="form-group">
          <%= form.label :name, '名前' %>
          <%= form.text_field :name, value: @contact.name, id: 'name', class: 'form-control-plaintext', :placeholder => '名前' %>
        </div>

        <div class="form-group">
          <%= form.label :email, 'メールアドレス' %>
          <%= form.text_field :email, value: @contact.email, id: 'email', class: 'form-control-plaintext', :placeholder => 'メールアドレス' %>
        </div>

        <div class="form-group">
          <%= form.label :content, 'お問い合わせ内容' %>
          <%= form.text_area :content, value: @contact.content, id: 'content', class: 'form-control-plaintext', :placeholder => 'お問い合わせ内容', :rows => '5' %>
        </div>

        <p>この内容で送信します。よろしいですか?</p>

        <div class="float-right">
          <%= form.submit "戻る", name: 'back', class: "btn btn-dark" %>
          <%= form.submit "送信", class: "btn btn-dark" %>
        </div>
      <% end %>

    </div>
  </div>
</div>

app/views/done.html.erb
<% provide :title, 'お問い合わせ' %>
<div class="container mt-4">
  <div class="row">
    <div class="col-md-12 col-lg-6 offset-lg-3">
      <h2><%= icon('fas', 'envelope') %> お問い合わせ</h2>
      <nav aria-label="パンくずリスト" class="small">
        <ol class="breadcrumb">
          <li class="breadcrumb-item"><%= link_to 'トップページ', root_path %></li>
          <li class="breadcrumb-item active" aria-current="page">お問い合わせ</li>
        </ol>
      </nav>
      <hr />

      <p>ありがとうございます。お問い合わせ内容を送信しました。</p>

    </div>
  </div>
</div>

メール送信設定

本番環境と開発環境で手順が異なります。

開発環境
「config/environments/development.rb」に以下を追記します。
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = { address: 'localhost', port: 1025 }
  config.action_mailer.raise_delivery_errors = true

本番環境
「config/environments/development.rb」に以下を追記します。
  config.action_mailer.default_url_options = {  host: 'localhost', port: 3000 }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.smtp_settings = {
    :address => "smtp.gmail.com",
    :port => 587,
    :user_name => ENV['MAIL_ADDRESS'],
    :password => ENV['MAIL_PASSWORD'],
    :authentication => :plain,
    :enable_starttls_auto => true
  }

環境変数の設定

送信先メールアドレスやパスワードをソースに直書きしてしまうとセキュリティ的に問題があるので、通常は環境変数に設定しておきます。
Gemfileに以下を追加し、bundle installを実行します。
gem 'dotenv-rails'
dotenv-railsを導入することで、.envに記載されている環境変数をENV['variable name']の形式で参照できるようになります。
.envを作成し以下を追加します。
「MAIL_ADDRESS」には送信先のメールアドレス(今回はGmail)、「MAIL_PASSWORD」にはGoogleのアプリパスワードを設定します。
MAIL_ADDRESS=Your mail address
MAIL_PASSWORD=Your mail password
.envをGithubなどのリモートサーバーにアップロードしてしまうと、やはりセキュリティ的に問題があるので、.envをコミット対象から除外します。
.gitignoreに以下を追記します。
/.env
.envをコミット対象から除外してしまうと、将来クローンしたときにどういう環境変数を設定したらいいかわからなくなってしまうので、環境変数名だけ記載したファイルを作成しておきます。
.env.sampleを作成し以下を追加します。
MAIL_ADDRESS=
MAIL_PASSWORD=

メール送信確認

ここまで行えばメール送信ができるようになっているはずです。最後にメール送信確認を行いましょう。

開発環境
開発環境では実際に設定したGmail宛に送信するのではなく、MailCatcherというGemが構築してくれるメールサーバー宛に送信します。
Gemfileに以下を追加し、bundle installを実行します。
gem 'mailcatcher'
ターミナルで以下のコマンドを実行します。
$ mailcatcher
ブラウザで「http://localhost:1080/」にアクセスすると、MailCatcherのメールボックスが表示されます。
この状態でお問い合わせフォームからメール送信するとMailCatcherのメールボックスにメールが届きます。

本番環境
デプロイ先がHerokuの場合、Herokuに環境変数を設定する必要があります。
ターミナルで以下を実行します。
$ heroku config:set MAIL_ADDRESS=Your mail address
$ heroku config:set MAIL_PASSWORD=Your mail password
本番環境では設定したGmail宛にメールが送信されるので、Gmailのメールボックスにメールが届くが確認します。

参考

関連記事

【Rails】Paranoiaを使用した論理削除(ソフトデリート)
# はじめに Paranoiaは、Railsアプリケーションで論理削除(ソフトデリート)を実現するためのGemです。 論理削除は、データベースのレコードを物理的に削除するのではなく、削除フラグを設定することで「削除済み」とみなす方法です。こ [...]
2024年7月20日 21:33
【Rails】activerecord-multi-tenantを使用したマルチテナントアプリケーションの作成
# はじめに マルチテナントアプリケーションでは、複数の顧客(テナント)が同じアプリケーションを利用するため、データの分離が必要です。 activerecord-multi-tenantは、このようなマルチテナント環境をサポートするための便 [...]
2024年7月18日 16:50
【Rails】RubyとRailsにおけるattr_reader, attr_writer, attr_accessorの概念と使用方法
# はじめに RubyとRailsの開発において、`attr_reader`,`attr_writer`,`attr_accessor`は非常に便利なメソッドです。これらは、クラス内でインスタンス変数に対するゲッターおよびセッターメソッドを簡単に [...]
2024年7月17日 18:11
【Rails】RubyとRailsにおけるyieldの概念と使用方法
# はじめに RubyとRailsにおける`yield`は、メソッドやテンプレートの中で動的にコードブロックを実行する能力を提供し、これによってコードの再利用性と拡張性が大幅に向上します。本記事では、RubyとRailsにおける`yield`の概 [...]
2024年7月17日 13:15
【Rails】AASMを使用してオブジェクトの状態遷移を効率的に管理
# はじめに Railsアプリケーションにおいて、オブジェクトの状態管理は重要な課題の一つです。AASM (Acts As State Machine) gemは、複雑な状態遷移を効率的に管理します。本記事では、AASMの基本的な使い方を解説して [...]
2024年7月16日 18:00
【Rails】RSpec + Swagger + rswagでアプリケーションのAPIをテストおよびドキュメント化する方法
# はじめに Railsアプリケーションの開発において、APIのテストとドキュメント化は重要な要素です。 RSpecはテストフレームワークとして広く利用されており、SwaggerはAPIの設計とドキュメント化を支援します。これらを統合するr [...]
2024年7月16日 14:27
【Rails】mailcatcherを使用して開発環境でメール送信をテストする方法
# はじめに mailcatcherは、開発環境でのメール送信をキャプチャするためのツールです。ローカルで送信されたメールをブラウザ上で簡単に確認できるようにします。mailcatcherをRailsアプリケーションで使用する方法について説明しま [...]
2024年7月15日 16:37
【Rails】impressionistを使用してページビューやクリック数を追跡する方法
# はじめに impressionist Gemを使用してRailsアプリケーションでページビューやクリック数を追跡する方法について説明します。 # 実装方法 ## impressionist Gemのインストール まず、impre [...]
2024年7月15日 14:18