PHPMailerでメール認証を簡易的に実装する

# はじめに
## 前提
- PHP がインストールされていること
- Composer を導入していること (推奨)
- 認証システムに利用するメールを準備しておくこと

上記は省くので各自でお願いします。

### Composer
https://getcomposer.org/download/

## 環境
OS: Windows 11 23H2 (10.0.22631)
XAMPP for Windows: 3.3.0
PHP: 8.2.12
Composer: 2.8.4 
PHPMailer: 6.9
メール: G-Mail

## ファイル構成

- vendor/
  - phpmailer/*
  - composer/*
  - autoload.php
- composer.json
- form.html - メールアドレス入力ページ
- send.php - メール送信用ページ
- verify.php - 認証用ページ
- users.json (send.phpで生成される) - 簡易的なアカウント管理のためのjson

実際に作成するのはform.html、send.php、verify.phpの3つです。
説明するために3つに分けているだけで1つに統合してもらっても構いませんし、この記事に含まれるPHPやHTMLやらは煮るなり焼くなり好きに自由にどうぞ

CSSはテキトーに作ってstyleタグとして埋め込んでます。

# PHPMailer のインストール
https://github.com/PHPMailer/PHPMailer

Composer を入れている場合は以下をターミナルに打ち込むと導入できます。
```sh
composer require phpmailer/phpmailer
```

# ウェブの作成

### 入力フォームのHTML
styleはおまけみたいな感じなのでテキトーです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1015774/c83e7f86-7b52-54e3-1132-aa728db94af0.png)

```form.html
<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8" />
		<title>アカウント登録</title>
		<style>
			body {
				font-family: Arial, sans-serif;
				font-size: 16px;
			}
			
			h1 {
				font-weight: normal;
				border-bottom: solid 1px #000;
			}
			
			input[type="text"] {
				padding: 6px;
				color: #000;
				background-color: #fff;
				border: solid 1px #444;
				border-radius: 0;
				font-size: 20px;
				outline: none;
				transition: border 0.5s;
			}
			
			input[type="text"]:focus {
				border: solid 1px #48f;
			}
			
			input[type="submit"] {
				padding: 3px 12px;
				color: #fff;
				background-color: #48f;
				border: solid 1px #444;
				border-radius: 0px;
				font-size: 20px;
				outline: none;
				transition: border 0.5s, color 0.5s, background-color 0.5s;
			}
			
			input[type="submit"]:hover {
				color: #fff;
				background-color: #000;
				border: solid 1px #666;
			}
		</style>
	</head>
	<body>
		<h1>アカウント登録</h1>
        <!-- 入力フォーム -->
		<form action="send.php" method="post">
			<p>以下にメールアドレスを入力して送信ボタンを押した後、送信されたメールをご確認ください。</p>
			<input type="text" name="email" placeholder="example@example.com">
			<input type="submit" value="送信" />
		</form>
	</body>
</html>
```

name属性が `email` の input の値は phpでは`$_POST['email']` に入ります。

### メールを送信するためのPHP
送信されるメール
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1015774/5fb868fb-5f2f-1ba8-44c8-2046dd4bf25b.png)

認証先のURLやサーバー設定、メール設定、件名、本文は必要に応じて変えてください。

```send.php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

require 'vendor/autoload.php';

mb_language("japanese");
mb_internal_encoding("UTF-8");

// $_POST['email'] が存在しない場合は処理を終える
if (!isset($_POST['email'])) 
    exit;

$email = $_POST['email']; // 入力フォームで入力されたメールアドレス
$token = uniqid();        // 時刻ベースの識別子を生成

// アカウントのデータ保存先
$filepath = "./users.json";

$users = [];
if (file_exists($filepath))
	$users = json_decode(file_get_contents($filepath), true);

// 既に使われているメールアドレスの場合は処理を終える
foreach ($users as $user) {
	if ($user['email'] == $email) {
		echo "既にそのメールアドレスは使われています。";
		exit;
	}
}

$users[] = [
	"email" => $email,
	"token" => $token,
	"status" => "pending",
];

file_put_contents($filepath, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

// ----

$mail = new PHPMailer(true);

try {
    // SMTPサーバー設定
    $mail->isSMTP(); // SMTPを利用
    $mail->Host = "smtp.gmail.com"; // ホスト名
    $mail->Port = 587; // ポート名
    $mail->SMTPSecure = "tls"; // TLSの利用
    $mail->SMTPAuth = true; // SMTP認証の利用
    $mail->Username = "example@gmail.com"; // ユーザー名
    $mail->Password = "..."; // パスワード
    /* 
        パスワードはGmailの場合、アプリパスワードとなるそうです。
    */

    // メール設定
    $mail->setFrom("xxx@gmail.com", "xxx Team"); // 送信者, 名前
    $mail->addAddress($email); // 宛先
    $mail->isHTML(true); // HTMLの利用
    $mail->CharSet = "UTF-8"; // 文字コードをUTF-8とする
    
     // 件名
    $mail->Subject = "アカウント登録を完了するにはメール認証をしてください";

    // 認証先のURL (本文に埋め込み)
    $url = "https://.../verify.php?t={$token}";

    // 本文
    $mail->Body = <<<EOD
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ja">
	<head>
		<meta charset="utf-8" />
		<title>アカウント登録</title>
		<style type="text/css">
			* {
				color: #000;
				font-family: Arial, sans-serif;
				text-align: center;
			}
			
			a.btn {
				padding: 10px 50px;
				color: #fff;
				background-color: #48f;
				border: none;
				font-size: 20px;
				outline: none;
				text-decoration: none;
				transition: border 0.5s, color 0.5s, background-color 0.5s;
			}
		</style>
	</head>
	<body>
		<h1>メール認証</h1>
		<p>
			こんにちは、○○○チームです。<br />
			メールアドレスを認証してアカウントの登録を完了しましょう。
		</p>
		<a href="{$url}" class="btn">メールを認証する</a>
		<p>もしくは、以下のリンクをコピーしてブラウザに貼り付けて開くこともできます。</p>
		<a href="{$url}">{$url}</a>
	</body>
</html>
EOD;

    $mail->send();
} catch (Exception $e) {
    echo "エラー: " . $mail->ErrorInfo;
}
```

### メールに送信される認証用のPHP
`https://..../verify.php?t=(send.phpで生成されたトークン)`へアクセスすると認証される仕組み

```verify.php
<?php
$token = $_GET['t']; // ?t=(トークン) からトークンを取得する

// アカウントのデータ保存先
$filepath = "./users.json";

if (!file_exists($filepath)) {
	echo "エラー: users.json が存在しません。";
	exit;
}

$users = json_decode(file_get_contents($filepath), true);

foreach ($users as $index => $user) {
	if ($user['token'] == $token) {
         // 既に認証されたメールアドレスの場合は処理を終える
		if ($user['status'] != "pending") {
			echo "エラー: そのメールアドレスは既に認証されています。";
			exit;
		}
		
		$users[$index]['status'] = "verified";
		break;
	}
}

file_put_contents($filepath, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo "メールが認証されました。";
```

## users.json について
今回は以下のようにjsonで保存します。DBを用いることも可能なので大規模なものを想定しているのであればjsonでは不十分かもしれませんね。

statusについてはpendingのときは保留中となり、メール認証後、verifiedとなります。

トークンにはuniqid()を利用してますが、短めで重複する可能性のあるものであるため、一時的保存ならば問題ないのですが、半永久的に利用するのであれば実際にはもっとよりよいものを使用すべきです。
```json
[
    {
        "email": "aaa@xxx.com",
        "token": "675c3a8582d8e",
        "status": "pending"
    },
    {
        "email": "bbb@xxx.com",
        "token": "675c3b4dbbb47",
        "status": "pending"
    },
    {
        "email": "ccc@yyy.jp",
        "token": "675c3b88d12b1",
        "status": "verified"
    }
]
```

# 最後に
開発しているウェブサービスにそろそろメール認証を取り入れようと思ったのでPHPMailerを初めて使ってみました。
メール認証をつくってみるのってなんだかロマンがありますね