【Rails】リマインダーメールの実装

## はじめに
個人開発において、ユーザーにリマンダーメールを送信したいと思ったのですが、体系的にまとめられているドキュメントを見つけるのに苦労しました。
自分と同じような初学者だとつまずく部分もあるかと思いますので、本記事でモデルの作成から順に説明していきます。

## やりたいこと
- 毎日、日本時刻AM9:00に、開始時刻が次の日になっているスケジュール（以下、イベントと言います。）を登録したユーザーにメールでリマインドする。
- 各ユーザーは、リマインド機能のオンオフを切り替えられる。（初期状態はオン）
- メール送信側はGmailを使用
## 開発環境
Ruby 3.1.2
Rails 6.1.6
## 実装
### 【モデル】
今回、必要となるモデルは、以下の2つです。
- ユーザー(User)モデル：どのメールアドレスに送信するのか、リマインド機能のオンオフ(deviseを使用)
    - name(ユーザー名) :string
    - email_receiving_activation(リマインド機能のオンオフ) :boolean
    - メールアドレスやパスワードはdeviseで自動生成される
     
- イベント（Event）モデル：ユーザーが登録したイベントと、その開始時刻を保存する
    - title(イベント名) :string
    - start_at(開始時刻) :datetime
    - user(アソシエーション) :references
```:ターミナル
rails g devise User name:string email_receiving_activation:boolean
rails g model Event title:string start_at:datetime user:references
```
モデルを作成したら、マイグレーションファイルへのNotNull制約やデフォルト値の設定、モデルファイルへのアソシエーションを記述します。
```ruby:db/migrate/##########_devise_create_users.rb
  class DeviseCreateUsers < ActiveRecord::Migration[6.1]
    def change
      create_table :users do |t|
        t.string  :email,                      null: false, default: ""
        t.string  :encrypted_password,         null: false, default: ""
        t.string  :name,                       null: false, default: ""
        t.boolean :email_receiving_activation, null: false, default: true # 初期状態はtrue（リマインダー機能がオン）の状態
      ...
      end
    ...
    end
  end
```
```ruby:db/migrate/##########_create_events.rb
class CreateEvents < ActiveRecord::Migration[6.1]
  def change
    create_table :events do |t|
      t.references :user,    null: false, foreign_key: true
      t.string :title,       null: false
      t.datetime :start_at,  null: false
    end
  end
end
```
```:ターミナル 
rails db:migrate
```

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :events, dependent: :destroy
end
```

```ruby:app/models/events.rb
class Event < ApplicationRecord
  belongs_to :user
end
```
### 【タイムゾーンの変更】
デフォルトで設定されているタイムゾーンはUTC（協定世界時）になっています。これを日本時間に変更しておきましょう。
```ruby:config/application.rb
module Portfolio
  class Application < Rails::Application
  ...
   config.time_zone = "Asia/Tokyo" # これを追加
  ...
  end
end
```
### 【データの作成】
次にデータを作成します。WEBアプリケーションとしては、コントローラやビューを用いてデータを作成するのが一般的ですが、今回は本質と異なるため、その工程を省き、コンソールでデータを作成します。
```:コンソール(rails c)
irb(main):001:0> User.create(name: "test_user", email: "test_user@email", password: "password")
=> #<User id: 1, name: "test_user", email: "test_user@email", email_receiving_activation: true, created_at: "2022-08-23 10:45:50.800011000 +0900", updated_at: "2022-08-23 10:45:50.800011000 +0900">
irb(main):002:0> Event.create(title: "test_event", start_at: "2022-08-31 13:00:00.000000000 JST +09:00"", user_id: 1)
=> #<Event:########
 id: 1,
 user_id: 1,
 title: "test_event",
 start_at: " Wed, 31 Aug 2022 13:00:00.000000000 JST +09:00">
```
これで、ユーザーと2022年8月31日13時のイベントが作成できました。

### 【メールの送信設定】
メールを送信するためには、Action Mailerという仕組みを活用します。
まず、ターミナルでMailerを作成します。
```:ターミナル
rails g mailer RemaindEventMailer
```
これで`app/mailers/remaind_event_mailer.rb`が作成されますので、その中にメールを送信するメソッドを記述してください。
```ruby:app/mailers/remaind_event_mailer.rb
class RemaindEventMailer < ApplicationMailer
  # ↓ここから追加
  def creation_email(event)
    @event = event # メールのテンプレを作成するときに使用します
    mail(
      subject: "【Converce】イベントリマインダー", # メールの件名
      to: event.user.email,                # メールの宛先
    )
  end
end
```
次にユーザーに送信されるメールのテンプレートを作成します。
`app/views/remaind_event_mailer`に`creation_email.html.erb`を作成し、自分の好きなようにテンプレートを記述してください。一例を以下に示しておきます。
`app/mailers/remaind_event_mailer.rb`で`@event`を定義しているので、titleやstart_atを表示することができます。
```erb:app/views/remaind_event_mailer/creation_email.html.erb
<p>登録されたイベントのご予定日が近づいて参りましたので、ご連絡いたします。</p>
<h2><%= @event.title %></h2>
<p>
  <span>●開始予定日時：<%= @event.start_at.strftime('%Y/%m/%d %H:%M') %></span><br>
</p>
```

その次に、今回はGmailからメールを送信するので、そのための設定を行います。
まずは、アプリパスワードというものが必要となるため、Gmail側でそちらを取得します。手順は、[こちらの記事](https://qiita.com/ozackiee/items/f260f34db4e5c887460d)が非常に参考になります。
アプリパスワードの取得が完了したら、Rails側の設定を行います。以下のように記述してください。
```ruby:config/environments/development.rb
require "active_support/core_ext/integer/time"

Rails.application.configure do
  ...
  config.action_mailer.default_url_options = {  host: "localhost", port: 3000 }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    address: "smtp.gmail.com",
    domain: "gmail.com",
    port:587,
    user_name: ENV['MAIL_USER_NAME'], #送信元となるメールアドレス（xxxx@gmail.com)
    password: ENV['MAIL_PASSWORD'], #アプリパスワード
    authentication: :login
  }
  ...
end
```
`addres`や`port`の記述については、お約束のようなものになります。
なお、本番環境の場合には、`config.action_mailer.default_url_options`の部分をドメイン名などにすればよいです。
また、`user_name`や`password`はセキュリティの観点から`.env`ファイルに記述しておきましょう。

これでメールを送信する準備が完了しました。

### 【バッチ処理】
最初に記述したように、今回は毎日AM9:00に次の日のスケジュールをリマインドしたいので、定時的に処理を実行するようバッチ処理を実装します。
まずは、`lib`フォルダの配下に`batch`フォルダを作成し、その中に`remaind_event.rb`を作成してください。そして、どのような処理をするのかをファイルの中に記述しましょう。
```ruby:lib/batch/remaind_event.rb
class Batch::RemaindEvent
  def self.remaind_event
    # Event.reload
    events = Event.all
    events.each do |event|
      time_difference = (event.start_at - Time.now.in_time_zone("Tokyo")) / 3600 # AM9:00との時間差を時間単位で算出する
      if time_difference <= 39 && time_difference >= 15  && event.user.email_receiving_activation == true # 次の日のスケジュールなのかユーザーがリマインド機能をオンにしているかを判定
        RemaindEventMailer.creation_email(event).deliver_now # メールを送信するためのメソッド
        p "メールを#{event.user.name}に送信しました" # ログに表示するメッセージ
      end
    end
  end
end
```
処理内容を細かく説明しますと、以下のようになります。
1. Eventモデルに存在する全てのデータを取得してeachメソッドを実行
1. `time_defference`にてプログラム実行時刻（AM9:00）とイベント開始時刻との時間差を時間単位で算出
1. AM9:00から見て、イベントの開始時刻が15時間以上39時間以内であれば、次の日のイベントであるため、if文の条件に記述
1. さらに、ユーザーがリマインド機能をオンにしていることも条件になるため、併せてif文に記述
1. `RemaindEventMailer.creation_email(event).deliver_now`で即時的にメールを送信する
1. 実行が完了したら、ログにメッセージを表示させる

この時点で、処理自体は実行できるようになったので、ターミナルで以下のコマンドを実行すると、メールが送信されます。
```:ターミナル
bundle exec rails runner Batch::RemaindEvent.remaind_event
```
問題なく実行されていれば、ログの`メールを#{event.user.name}に送信しました`が表示されるはずです。

次に、定時的に実行させるためのバッチ処理を実装します。こちらには、`cron`という常駐プログラムを使用します。`cron`については、[こちらのサイト](https://wa3.i-3-i.info/word11748.html)での説明がわかりやすいかと思います。
そして、cronのスケジュール管理をするには、gemの`whenever`を使用します。Gemfileに追加してください。
```ruby:Gemfile
gem 'whenever', require: false
```
インストールするとともに、スケジュールファイルを作成するために以下のコマンドを実行してください。
```:ターミナル
bundle install
bundle exec wheneverize
```
`config/schedule.rb`が作成されるので、その中に実行するスケジュールを記述します。
```ruby:config/schedule.rb
require File.expand_path(File.dirname(__FILE__) + "/environment")
rails_env = Rails.env.to_sym
set :environment, rails_env
set :output, "log/cron.log" # ログをアウトプットする
every 1.day, at: '00:00' do # UTC時刻で00:00なので日本時刻09:00
  begin
    runner "Batch::RemaindEvent.remaind_event" # この処理を実行する
  rescue => e
    Rails.logger.error("aborted rails runner")
    raise e
  end
end
```
`every 1.day, at: '00:00'`で、毎日1回、UTC時刻で0:00(日本時刻でAM9：00)に実行するという意味になります。他のパターンもあるので、[wheneverのGitHub](https://github.com/javan/whenever)をご参照ください。

記述が完了したら、ターミナルで以下のコマンドを実行することでスケジュールが反映されます。
```:ターミナル
 bundle exec whenever --update-crontab
```
最後に、cronを再起動させましょう。
```:ターミナル
sudo systemctl start crond
```
これで毎日AM9:00にリマインダーメールが送信されるようになります。
補足ですが、ちゃんとバッチ処理が実行されているかを確認するには、`sudo tail -f log/cron.log`をターミナルで実行してください。`-f`を付けているので、監視している状態になります。抜け出したい時は、`Ctrl + C`を押してください。

## さいごに
以上になります。
少々ややこしい部分もありますが、ひとつひとつの理解はそんなに難しくないと思います。
スケジューラー機能を実装するのであれば、リマインダー機能はあった方が良いと思いますので、どなたかのお役に立てれば嬉しいです！！