PHPで作るお問い合わせフォーム送信処理|mb_send_mailとトークン生成の仕組みを読み解く


はじめに

これまでHTML・CSS・JavaScriptを中心に解説してきましたが、今回は実際にお問い合わせフォームのバックエンドで動くPHPコードを取り上げます。第二十四弾で実装したお問い合わせフォームのHTML・JavaScriptバリデーションに対して、今回はそのフォームから送信されたデータをサーバー側で受け取ってメール送信する処理を読み解いていきます。学習のアウトプットとして、自分が理解したことを整理しながら共有します。

⚠️ 今回掲載するコードはメールアドレス・会社名などの実在情報を伏せ字に置き換えています。 実務で使う際は自分のサーバー環境に合わせて書き換えてください。


1. このPHPコードが何をしているのか全体像

1.1 処理の流れをざっくり把握する

このコードは「お問い合わせフォームから送信された内容を受け取り、2通のメールを送る」という処理を行っています。

1. フォームから送信されたデータ($_POST)を受け取る
2. CSRF対策用のトークンを生成してセッションに保存する
3. 「自分宛て」のお問い合わせ内容メールを組み立てる
4. 「ユーザー宛て」の自動返信メールを組み立てる
5. まずユーザー宛てのメールを送信する
6. ユーザー宛てが成功したら、自分宛てのメールも送信する
7. 両方成功したらセッションを破棄する(二重送信防止)
8. 結果に応じたメッセージを画面に表示する
9. HTML側で送信された内容を一覧表示する

💡 全体としては「2通メールを送って画面に結果を表示する」というシンプルな構造です: コードの行数は多く見えますが、やっていることを大きく分けると「メールを組み立てる」「メールを送る」「結果を表示する」の3ステップに整理できます。


2. POSTで受け取った値の扱い方

2.1 $_POSTとは

HTMLの <form>method="post" を指定して送信されたデータは、PHPの $_POST という特別な配列に自動的に格納されます。

if (isset($_POST['company'])) {
    $company = $_POST['company'];
    $name1   = $_POST['name_1'];
    $name2   = $_POST['name_2'];
    $read1   = $_POST['read_1'];
    $read2   = $_POST['read_2'];
    $email   = $_POST['mail_address'];
    $phone   = $_POST['phone'];
    $kind    = $_POST['kind'];
    $contents = $_POST['mail_contents'];
}

2.2 isset()でチェックしている理由

isset($_POST['company']) は「$_POST['company'] という値が存在するかどうか」を確認しています。

状況isset($_POST['company']) の結果
フォームから正しく送信されたtrue(中の処理が実行される)
このPHPファイルに直接アクセスしたfalse(中の処理がスキップされる)

💡 isset() は「いきなりエラーで止まらないようにする安全装置」です: フォーム経由ではなくURLを直接開かれた場合、$_POST['company'] は存在しないため、チェックなしで使おうとするとエラーになります。isset() で先に確認することでエラーを防いでいます。

2.3 チェックボックス(複数選択)の受け取り方

「お問い合わせの種類」のように複数選択できる項目(チェックボックス)は配列として送られてきます。

if (isset($_POST['kind']) && is_array($_POST['kind'])) {
    $kind = implode("、", $_POST["kind"]);
}

implode("、", $配列) は配列の中身を「、」で連結して1つの文字列にする関数です。

$_POST['kind'] が ["資料請求", "見積もり依頼"] の場合
↓
implode("、", $_POST['kind'])
↓
"資料請求、見積もり依頼" という1つの文字列になる

2.4 ヒアドキュメントでメール本文を組み立てる

$contents = <<<EOD

              ◆会社名
              {$company}

              ◆お名前
              {$name1} {$name2}

              EOD;

<<<EOD ... EOD; という書き方はヒアドキュメントと呼ばれるPHPの構文です。複数行の文字列を改行やレイアウトをそのまま保ったまま書けるのが特徴です。

通常の文字列結合ヒアドキュメント
"◆会社名\n" . $company . "\n\n" のように .\n を多用する改行をそのまま書くだけで済む
長文になると読みにくいテンプレートのように見やすく書ける

3. トークン生成とセッションの役割

3.1 トークンとは何か・なぜ必要か

$token = sha1(uniqid(mt_rand(), true));
$_SESSION['token'] = $token;

このコードはCSRF(クロスサイトリクエストフォージェリ)対策のためのトークンを生成しています。CSRFとは、悪意のある第三者が利用者になりすまして勝手にフォームを送信させてしまう攻撃のことです。

トークンを使ったCSRF対策の考え方:

1. フォームを表示するときにランダムな文字列(トークン)を発行する
2. そのトークンをセッションとフォームの隠しフィールド(hidden)の両方に保存する
3. フォーム送信時、セッションのトークンとフォームから送られたトークンを照合する
4. 一致すれば「正規のフォームから送信された」と判断して処理を進める
5. 一致しなければ処理を拒否する

sha1(uniqid(mt_rand(), true)) の部分は、推測されにくいランダムな文字列を作るための関数の組み合わせです。

関数役割
mt_rand()ランダムな数値を生成する
uniqid(..., true)その数値をもとに、より一意性の高いID文字列を作る
sha1(...)できた文字列をハッシュ化してさらに推測困難にする

3.2 session_start()とは

ファイルの一番先頭にある session_start();セッションを使うための準備です。

session_start();   //SESSIONを使うときは最初にスタートさせる

セッションとは ユーザーごとの情報をサーバー側で一時的に記憶しておく仕組みです。例えば「このブラウザのユーザーは、ついさっき正しい手順でこのフォームのページを開いた」という情報を覚えておくために使われます。

⚠️ 今回のコードには注意点があります: トークンを生成して $_SESSION['token'] に保存している部分はありますが、フォーム送信時にそのトークンを照合する処理が見当たりません。CSRF対策として機能させるには、HTML側のフォームに <input type="hidden" name="token" value="..."> を出力し、送信時に $_POST['token']$_SESSION['token'] を比較する処理が必要です。この点は後の「実務で気をつけるべきポイント」で改めて触れます。

3.3 メール送信成功後にセッションを破棄している理由

$_SESSION = [];
if (isset($_COOKIE[session_name()])) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000, $params["path"], $params["domain"], $params["secure"], $params['httponly']);
}
session_destroy();

メール送信が成功した後にセッションを空にして破棄しています。これはフォームの二重送信を防ぐための処理です。

セッション破棄の意味:

1. 送信成功 → セッション情報を空にする($_SESSION = [])
2. ブラウザに保存されているセッションCookieも削除する
3. session_destroy()でサーバー側のセッションデータも完全に削除する

→ ユーザーがブラウザの「戻る」ボタンで再送信しようとしても
  古いセッション情報は無効になっているため、
  意図しない二重送信を防ぎやすくなる

4. mb_send_mailを使ったメール送信の仕組み

4.1 mb_send_mailとは

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

$param = "-f" . $to;

if (mb_send_mail($to2, $mailtitle2, $contents2, $from2, $param)) {
    // 送信成功時の処理
}

mb_send_mail() はPHPに標準で用意されているメール送信関数 mail()多言語対応版です。日本語のような英語以外の文字を含むメールを正しく送るために使われます。

mb_send_mail( 送信先, 件名, 本文, 追加ヘッダー, 追加パラメータ )
引数役割
第1引数($to2メールの送信先アドレス
第2引数($mailtitle2メールの件名
第3引数($contents2メールの本文
第4引数($from2送信元アドレスなどの追加ヘッダー
第5引数($paramサーバーに渡す追加コマンドラインパラメータ

4.2 mb_language・mb_internal_encodingの役割

mb_language("Japanese");      // 日本語のメールとして扱う設定
mb_internal_encoding("UTF-8"); // 文字コードをUTF-8に統一する

この2行を mb_send_mail() の前に書いておくことで、日本語の件名・本文が文字化けせずに送信されます。

4.3 -fパラメータの意味

$param = "-f" . $to;

-f はメール送信時のエンベロープFrom(配送エラーの通知先など、メールサーバーが内部的に使うアドレス)を指定するパラメータです。送信元を明示することで、迷惑メールと判定されにくくする効果があります。


5. 自分宛てメールとユーザー宛て自動返信メールの違い

5.1 2通のメールが送られる理由

このコードでは2通の異なるメールが送信されます。

メール宛先目的
ユーザー宛て自動返信メール$to2 = $email(フォームに入力されたメールアドレス)お問い合わせを受け付けたことをユーザーに知らせる
自分宛てお問い合わせ内容メール$to = "(運営者のメールアドレス)"運営者がお問い合わせ内容を確認できるようにする

5.2 自分宛てメールの内容

$to = "(運営者のメールアドレス)";
$mailtitle = "{$name1} {$name2}様よりお問い合わせが届きました。";

$contents = <<<EOD
              ◆会社名
              {$company}
              ◆お名前
              {$name1} {$name2}
              (以下省略)
              EOD;

$from = "Return-Path: " . $to . "\r\n";
$from = $from . 'From: ' . $email; // 送信元はユーザーのメールアドレス

運営者が受け取るこのメールでは、From: にユーザー自身のメールアドレスが設定されています。これにより、運営者がメールソフトでそのまま「返信」をクリックすればユーザー宛てに返信できる、という配慮がされています。

5.3 ユーザー宛て自動返信メールの内容

$to2 = $email;  // 送信先はフォームに入力されたメールアドレス
$mailtitle2 = "【自動送信】受付を完了いたしました。";

$contents2 = <<<EOD
            お問い合わせありがとうございます。
            以下の内容を送信いたしました。
            (送信内容を再掲)
            E-mail: (運営者の問い合わせ先メールアドレス)
            サイト運営者:(運営会社名)
            EOD;

$from2 = "Return-Path:" . $to . "\r\n";
$from2 = $from2 . 'From: ' . $to;  // 送信元は運営者のメールアドレス

ユーザーが受け取るこちらのメールは、件名に「【自動送信】」と明記され、送信した内容の控えと運営者の連絡先が記載されています。

💡 同じ$contentsを使い回している点に注目しましょう: 自分宛てメールで組み立てた $contents(問い合わせ内容)が、ユーザー宛てメールの本文にもそのまま埋め込まれています。一度組み立てたデータを2通のメールで再利用する効率的な書き方です。


6. 送信成功時と失敗時の処理

6.1 送信処理のネスト構造を整理する

if (mb_send_mail($to2, $mailtitle2, $contents2, $from2, $param)) {
    // ① ユーザー宛てメールの送信に成功した場合

    $message = '確認メールを送信しました...';

    if (mb_send_mail($to, $mailtitle, $contents, $from, $param)) {
        // ② さらに自分宛てメールの送信にも成功した場合
        // セッションを破棄する
    } else {
        // ③ ユーザー宛ては成功したが、自分宛ての送信に失敗した場合
        $message = '何らかの理由で送信エラーが発生しました...';
    }

} else {
    // ④ そもそもユーザー宛てメールの送信に失敗した場合
    $message = '確認メールを送信できませんでした...';
}

この if の入れ子構造(ネスト)を整理すると、以下の4パターンの結果に分岐していることがわかります。

パターンユーザー宛て自分宛て$message の内容
成功成功送信完了メッセージ+セッション破棄
成功失敗エラーメッセージ
失敗(実行されない)メールアドレス不備の可能性を伝えるメッセージ

💡 「先にユーザー宛てを送る」という順序に意味があります: もしユーザー宛てメールが送れない場合、それは多くの場合「メールアドレスの入力ミス」が原因です。先にユーザー宛てを試すことで、入力ミスの可能性をユーザーにいち早く伝えられる設計になっています。

6.2 メッセージをHTMLとして組み立てている点に注意

$message = '<p class="question-text">『' . $email . '』宛に確認メールを送信しました<br>お問い合わせありがとうございます。</p>';

$message の中に <p> タグや <br> タグがそのまま含まれています。これは後でHTML側に echo $message; のような形で出力することを前提にした書き方です。


7. HTML側で送信内容を表示する仕組み

7.1 入力内容を確認画面として再表示する

<dt>会社名<span>Company Name</span></dt>
<dd><?php echo $_POST["company"]; ?></dd>

<dt>名前<span>Your Name</span></dt>
<dd class="required"><?php echo $_POST["name_1"]; ?> <?php echo $_POST["name_2"]; ?></dd>

<?php echo $_POST["company"]; ?> のように、PHPのタグをHTMLの中に直接埋め込むことで、フォームに入力された内容をそのまま画面に表示しています。これは「送信した内容を確認できる画面」を作るための一般的な書き方です。

7.2 お問い合わせの種類だけ別の変数を使っている理由

<dt>お問い合わせの種類<span>Inquiry Kind</span></dt>
<dd><?php echo $kind; ?></dd>

他の項目は $_POST["○○"] を直接表示していますが、この項目だけ $kind という変数を使っています。これは前述の通り、チェックボックスの配列を implode() で1つの文字列に変換した結果を表示するためです。$_POST['kind'] は配列のままなのでそのまま echo で表示することができません。


8. 実務で気をつけるべきポイント

学習用としてこのコードを読み解いてみましたが、実務でそのまま使う場合にはいくつか注意すべき点があります。

8.1 バリデーション(入力チェック)が見当たらない

// このコードには以下のようなチェックが含まれていない
if (empty($_POST['mail_address'])) {
    // メールアドレスが空の場合のエラー処理
}

if (!filter_var($_POST['mail_address'], FILTER_VALIDATE_EMAIL)) {
    // メールアドレスの形式が正しいかのチェック
}

第二十四弾で解説したJavaScript側のバリデーションに加えて、サーバー側でも同じチェックを行うことが重要です。JavaScriptは無効化されていたり、フォームを経由せず直接送信されたりする可能性があるためです。

8.2 サニタイズ(無害化)が見当たらない

// 入力値をそのままHTMLに表示する前に変換が必要
$company = htmlspecialchars($_POST['company'], ENT_QUOTES, 'UTF-8');

<?php echo $_POST["company"]; ?> のように、ユーザーの入力値をそのままHTMLに出力すると**XSS(クロスサイトスクリプティング)**という脆弱性につながる可能性があります。悪意のあるユーザーが会社名の欄に <script> タグを含む文字列を入力した場合、それがそのままページに埋め込まれて実行されてしまう危険があります。

⚠️ ユーザー入力をHTMLに出力する箇所は必ず htmlspecialchars() を通しましょう: <>"' などの特殊文字を無害な文字列に変換することで、悪意のあるコードの実行を防げます。

8.3 CSRFトークンの照合処理が未実装

第3章で触れた通り、トークンを生成・保存はしているものの照合する処理がありません。本来は以下のような処理が必要です。

// フォーム送信時にトークンを照合する例
if (!isset($_POST['token']) || $_POST['token'] !== $_SESSION['token']) {
    die('不正なアクセスです');
}

8.4 メールヘッダーインジェクション対策

$from = $from . 'From: ' . $email;

ユーザーが入力したメールアドレスをそのまま From: ヘッダーに使っていますが、改行コードを含む不正な値が入力された場合にヘッダーインジェクションという攻撃を許してしまう可能性があります。filter_var() でメールアドレスとして正しい形式かを確認してから使うことが推奨されます。

8.5 実務で気をつけるべきポイントまとめ

項目対応方法
バリデーションサーバー側でも empty()filter_var() で必須項目・形式をチェックする
サニタイズ出力前に必ず htmlspecialchars() を通す
CSRF対策生成したトークンをフォームに埋め込み、送信時に照合する
メールヘッダーインジェクションメールアドレスの形式チェックを行ってからヘッダーに使う
確認画面の信頼性確認画面を経由せず直接 send.php にPOSTされるケースも考慮する

9. 学習チェックリスト

$_POSTisset() を組み合わせる理由を説明できるか

チェックボックスの値が配列で届くことを理解しているか

ヒアドキュメント(<<<EOD)の書き方を理解しているか

セッションとトークンがCSRF対策にどう関わるか説明できるか

mb_send_mail() の5つの引数の役割を説明できるか

自分宛てメールとユーザー宛てメールで From が違う理由を理解しているか

送信成功・失敗のネストされた if 文の分岐を整理できるか

サーバー側バリデーション・サニタイズ・CSRFトークン照合が抜けている点に気づけたか


まとめ

今回は実際に動くPHPのお問い合わせフォーム送信処理を読み解きながら、学んだ内容を整理しました。ポイントをまとめると:

  • $_POST で受け取ったデータは isset() で存在確認してから使う
  • チェックボックスの複数選択値は配列で届くため implode() で文字列に変換する
  • トークンとセッションはCSRF対策の仕組みだが、今回のコードには照合処理が抜けている
  • mb_send_mail() は日本語メールを文字化けなく送るための関数で、5つの引数それぞれに役割がある
  • 自分宛てメールとユーザー宛て自動返信メールは From ヘッダーの使い方が異なる
  • 送信結果は2段階の if の入れ子で4パターンに分岐している
  • 実務で使う場合はバリデーション・サニタイズ・CSRFトークンの照合・メールヘッダーインジェクション対策を追加する必要がある

PHPの実コードを読み解くことで、これまで学んできたHTML・JavaScriptの知識がサーバーサイドとどうつながるのかが見えてきました。次の記事では、今回見つかったバリデーション・サニタイズ・CSRF対策の不足を実際に補強したコードへの改善案を解説します。お楽しみに!

コメント

タイトルとURLをコピーしました