Laravel+mailhog　ローカル環境でメール送信機能の動作確認(docker)

## はじめに
LaravelでECサイトを構築中にメール送信機能の実装後、メールの送受信の動作確認をローカル環境で行いたい場面がありました。dockerを使ってテスト用メールサーバーを構築しようと思ったのですが、ネット上のソースは、docker composeを使って複数のdockerコンテナを連携させる想定のものばかりで、メールサーバー単体をdockerで構築してローカルのLaravelアプリケーションに連携させるものがなかったので、調べて実装したことをここに投稿したいと思います。

## 動作環境
OS macOS Monterey
Laravel 9.31.0
php 8.0.8
mysql 5.7.34
mailhog 1.0.1
docker-engine 20.10.8

## Mailhog(テスト用メールサーバー)
Mailhogは、開発者向けのメール送受信テスト用ツールです。
Mailhogとアプリケーションを連携させる場合は、メールサーバーの設定をSMTPに設定する必要があります。
メール送受信結果については、MailhogのUIページまたはJSON APIで取得可能です。

デフォルトのポート番号は次の通りです。
- SMTPサーバー：1025
- HTTPサーバー：8025

メッセージはメモリ上に保存されます。

## Dockerを使ってローカル環境にメールサーバを構築
### Dockerfileを作成
~~~terminal:terminal
$ docker run -p 8025:8025 -p 1025:1025 mailhog/mailhog
~~~
上記のようにdockerhub上の公式イメージを使用するのが手取り早いのですが、今回は学習用に
[GitHub上に公開されている公式Mailhogドキュメント](https://github.com/mailhog/MailHog)にDockerfileが公開されているのでこれを使用します。
Dockerfileを使用する利点としては自己流にイメージをカスタマイズすることができることです。今回は特にカスタマイズしないのですが、dockerfileを作成してから構築までの流れを理解するために、dockerfileを使用してみました。
~~~dockerfile:dockerfile
#
# MailHog Dockerfile
#

FROM alpine:3.4

# Install ca-certificates, required for the "release message" feature:
RUN apk --no-cache add \
    ca-certificates

# Install MailHog:
RUN apk --no-cache add --virtual build-dependencies \
    go \
    git \
  && mkdir -p /root/gocode \
  && export GOPATH=/root/gocode \
  && go get github.com/mailhog/MailHog \
  && mv /root/gocode/bin/MailHog /usr/local/bin \
  && rm -rf /root/gocode \
  && apk del --purge build-dependencies

# Add mailhog user/group with uid/gid 1000.
# This is a workaround for boot2docker issue #581, see
# https://github.com/boot2docker/boot2docker/issues/581
RUN adduser -D -u 1000 mailhog

USER mailhog

WORKDIR /home/mailhog

ENTRYPOINT ["MailHog"]

# Expose the SMTP and HTTP ports:
EXPOSE 1025 8025
~~~

#### dockerfileについて解説
dockerエンジンはDockerfileを上から順に読み込み、命令を実行していきます。
1. FROM命令で、alpineという軽量なlinux　ディストリビューションのイメージを使用します。
1. 一番目のRUN命令でSSL接続をするためのCA証明書を取得するためのコマンドを実行しています。これがないとCAエラーが発生します。
1. 二番目のRun命令でMailhogをインストールしています。MailhogはGo言語で書かれているため『go』というコマンドが見えますね。
1. 三番目のRUN命令で、ユーザーとグループを追加しています。[boot2docker issue](https://github.com/boot2docker/boot2docker/issues/581)というへの対応みたいです。boot2dockerを使用している場合、docker側で作成したファイルのパーミションがroot権限のみとなってしまい、ホストからの編集ができない問題があったようです。ここでは新しくuidとgidが1000となるmailhogというgroupに所属するmailhogというuserを作成して、デフォルトユーザーに設定しています
1. USER命令でコマンドを実行するuserを先ほど作成したuserに設定します。
1. WORKDIR命令でこれに続くENTRYPOINT命令の実行ディレクトリを指定します。docker側にこのディレクトリが存在しない場合は、新しく作成されます。
1. ENTRYPOINT命令でコマンドを実行します。
1. EXPOSE命令で指定した番号のポートを開きます。

:::note info
RUN adduser -D -u 1000 mailhogについて
検証のため、docker側とローカルホスト環境をバインドマウントしてみたのですが、docker側でroot権限で作成したファイルにローカルのホスト環境から編集することができました。ホスト側を確認するとgroupがadminになっています。Macの場合ユーザーはstaffとadminグループに所属するのがデフォルトなので、root権限のdockerファイルを編集できたようです。EC2やその他のグループで編集できない等があれば、今回のようにデフォルトuserをdocker側で設定する必要がありそうです。
以下が検証結果です。
:::
~~~terminal:docker側　home/mailhog
/home/mailhog # touch fromdocker.txt
/home/mailhog # ls -l fromdocker.txt
-rw-r--r--    1 root     root             4 Oct  2 02:37 fromdocker.txt
~~~

~~~terminal:ホスト側　home/mailhog
MacBook-Pro:mailhog host$ ls -l fromdocker.txt
-rw-r--r--  1 host  admin  4 10  2 11:37 fromdocker.txt
~~~

### dockerfileからイメージをbuild
Dockerfileがあるディレクトリまで移動して、下記コマンドを実行します。
~~~terminal:terminal
docker build -t mailhog_sample　.
~~~
-tオプションでイメージにタグ名をつけます。こうすることで、コンテナを起動する際にどのイメージを使うかを名前で指定することができます。

### dockerコンテナを起動する。
~~~terminal:terminal
docker run -d -p 1025:1025 -p 8025:8025 mailhog_sample
~~~
-dオプションでデタッチモードでコンテナを起動します。こうすることで、シェルが占領されません。
-pオプションでローカル側とdocker側のポートをバインドさせます。
これで[http://localhost:8025](http://localhost:8025)にアクセスすれば、MailhogのUIページが表示されるようになっています。

## Laravelの.envファイルを編集
~~~env:.env
MAIL_MAILER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
~~~
MAIL_MAILERはMailhogはSMTPサーバーを使用するので『smtp』
MAIL_HOSTはローカル環境なので、『localhost』とします。
MAIL_PORTはSMTP用に開いているポート番号1025を設定します。

これで連携完了です！

## Mailableの生成
アプリケーションが送信する電子メールはMailableクラスとして表します。
これらのクラスはapp/Mailディレクトリに保存されます。
~~~terminal:terminal
php artisan make:mail Ordered
~~~
作成されたOrdered.phpを編集します。
~~~php:Ordered.php
<?php

namespace App\Mail;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class Ordered extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * 注文インスタンス
     *
     * @var \App\Models\Order
     */
    public $order;

    /**
     * 新しいメッセージインスタンスの生成
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * メッセージを作成
     *
     * @return $this
     */
    public function build()
    {
       return $this->from('ordered@example.com','株式会社hogehoge')
                    ->subject('注文完了のお知らせ')
                    ->view('emails.orders.ordered');
    }
}

~~~
>Mailableクラスを生成したら、それを開いて、その内容を調べてください。まず、メール可能なクラスの設定はすべてbuildメソッドで行われることに注意してください。このメソッド内でfrom、subject、view、attachなどのさまざまなメソッドを呼び出して、電子メールの表示と配信を設定できます。

引用:[Laravel 9.x メール
](https://readouble.com/laravel/9.x/ja/mail.html)
##### 以下の点を編集しました

- Orderモデルのインスタンスをpublicプロパティとして持たせる。こうすることで、viewにデータを渡すことができます。
- Orderedクラスがインスタンス化されるタイミングで、コンストラクタの引数でOrderモデルのインスタンスを初期化する処理を追記。
- buildメソッドでメッセージを作成。メール可能なクラスの設定はすべてbuildメソッドで行われます。送り先（第一引数がアドレス、第二引数が名前）、件名そして本文はresources/views/orders/ordered.bleade.phpの内容となります。

## コントローラーの設定
~~~php:OrderController.php
 public function sendEmail(){
         //Mailerでメールを送信
         $order = Order::find(1);//注文確定後の情報を取得
         Mail::to(Auth::user())->send(new Ordered($order));
         return view('order_finished');
    }
~~~
> メッセージを送信するには、Mailファサードでtoメソッドを使用します。toメソッドは、電子メールアドレス、ユーザーインスタンス、またはユーザーのコレクションを受け入れます。オブジェクトまたはオブジェクトのコレクションを渡すと、メーラーは電子メールの受信者を決定するときに自動的にemailおよびnameプロパティを使用するため、これらの属性がオブジェクトで使用可能であることを確認してください。受信者を指定したら、Mailableクラスのインスタンスをsendメソッドに渡すことができます。

引用:[Laravel 9.x メール
](https://readouble.com/laravel/9.x/ja/mail.html)

- $orderは、Orderモデルのインスタンス（id=1)で初期化しています。
- Mailファサードのtoメソッドには認証済みのユーザーインスタンスを引数に持たせます。ユーザーインスタンスは、emailプロパティを持つようにテーブルを設計していますので、Laravelが内部的にemailプロパティの値を取得してくれます。
- 最後にsendメソッドにMailableクラスのインスタンス（Ordered)を渡します。ここで作成されたメッセージの内容が受信者に届きます。Orderedのコンストラクタに$orderを渡しているのは、Viewにデータを渡すためです。

## Viewの作成
Mailableクラスのコンストラクタにpubulicプロパティ引数として渡しているため、変数$orderをbladeテンプレートで使用することができます。

~~~html:ordered.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body> 
    <h1>{{ $order->destination_name }}様　ご注文ありがとうございました。</h1>
    <h2>注文内容詳細</h2>
    <div>
        注文日時:{{ $order->created_at }}
    </div>
    <div>
        注文合計額:{{ number_format($order->total_price) }}円
    </div>
    <h3>配送先</h3>
    <div>
        郵便番号:{{$order->destination_zipcode}}
    </div>
    <div>
        住所:{{$order->destination_address}}
    </div>
    <div>
        連絡先:{{$order->destination_tel}}
    </div>
</body>
</html>
~~~

## ルーティングの設定
~~~php:web.php
Route::get('/sendEmail', [OrderController::class,'sendEmail']);
~~~
![スクリーンショット 2022-10-03 11.08.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653944/48bb6634-afb7-347f-3656-5733069eae8c.png)

## 参考
https://gist.github.com/Propaganistas/9755257e63301bbd5dd0dacbf4eed5ea
https://akrabat.com/using-mailhog-via-docker-for-testing-email/
https://github.com/mailhog/MailHog
https://readouble.com/laravel/9.x/ja/mail.html