PHP コンタクトフォーム(お問い合わせページ)の作り方

PHP を使った確認画面のあるコンタクトフォーム(お問い合わせページ)の作成方法についての覚書です。

ユーザが入力した値を保持しながらページ間を移動(遷移)するので、セッションを利用します。

セッションのセキュリティ対策としては、session_regenerate_id() 関数を使ってセッション ID を変更し、CSRF 対策としてセッションの開始時にトークン(ランダムな文字列)を発行し、そのトークンが一致する場合にのみ処理を実行するようにしています。

また、jQuery を使った入力値の検証や自動返信、PHPMailer を使ったメールの送信(Gmail SMTP サーバ)や reCAPTCHA v3 を使ったスパム対策の実装方法についても掲載しています。

関連ページ:PHP メールフォームの作り方(確認画面なし)

更新日:2020年05月13日

作成日:2020年3月30日

コンタクトフォームの作成

「入力ページ」 → 「確認ページ」 → 「完了ページ」の順で遷移するコンタクトフォームを作成します。

以下のサンプルでは実際にはメールは送信されません。

※上記サンプルは iframe を使って以下のサンプルを表示しているため連動する場合があります。

サンプルを別ページで開く

以下がそれぞれのページの概要です。

ページ 説明
入力ページ ユーザーが「名前」「Email」「電話番号」「件名」「問い合わせ内容」を入力するページです。必須項目や値のチェックは jQuery で行います。
確認ページ サーバ側で入力された値を検証して入力された値に問題がなければ入力内容と「送信ボタン」及び「戻るボタン」を表示します。不備がある場合はエラーを表示して再度入力フォームを表示します。
完了ページ 問い合わせの受付が完了したことを知らせるページで、問い合わせメールの送信結果を表示します。

フォルダ・ファイル構成

「入力ページ」「確認ページ」「完了ページ」は 「contact」というフォルダ内に配置しています。

また、この例では「libs」というフォルダを作成し、メールの送信情報や必要な関数を記述したファイルを保存し、.htaccesss で外部からアクセスできないようにしています。

必要に応じてファイル名やフォルダ名は適宜変更し、プログラム内の該当箇所も変更します。

├── contact
│   ├── contact.php    //入力ページ
│   ├── confirm.php  //確認ページ
│   └── complete.php //完了ページ
├── libs
│   ├── .htaccess //アクセス制御(このフォルダへの外部からのアクセスを拒否)
│   ├── functions.php //値を検証する関数やエスケープ処理をする関数のファイル
│   └── mailvars.php //メールの送信先などの情報を記述したファイル
└── style.css //スタイルシート

以下は libs フォルダへのアクセスを制御する .htaccesss というファイルです。

.htaccess
deny from all

以下は PHP で入力値を検証する際に使用する関数を記述した functions.php というファイルです。

functions.php
<?php
//エスケープ処理を行う関数
function h($var) {
  if(is_array($var)){
    //$varが配列の場合、h()関数をそれぞれの要素について呼び出す(再帰)
    return array_map('h', $var);
  }else{
    return htmlspecialchars($var, ENT_QUOTES, 'UTF-8');
  }
}

//入力値に不正なデータがないかなどをチェックする関数
function checkInput($var){
  if(is_array($var)){
    return array_map('checkInput', $var);
  }else{
    //NULLバイト攻撃対策
    if(preg_match('/\0/', $var)){  
      die('不正な入力です。');
    }
    //文字エンコードのチェック
    if(!mb_check_encoding($var, 'UTF-8')){ 
      die('不正な入力です。');
    }
    //改行、タブ以外の制御文字のチェック
    if(preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $var) === 0){  
      die('不正な入力です。制御文字は使用できません。');
    }
    return $var;
  }
}

以下はメールの送信処理の際に使用する送信情報などを記述した mailvars.php というファイルです。

mailvars.php
<?php
//メールの宛先(To)の Email アドレス
define('MAIL_TO', "info@xxxxx.com");
//メールの宛先(To)の名前  
define('MAIL_TO_NAME', "宛先の名前 ");
//Cc の名前
define('MAIL_CC', 'xxxx@xxxxxx.com');
//Cc の名前
define('MAIL_CC_NAME', 'Cc宛先名');
//Bcc
define('MAIL_BCC', 'xxxxx@xxxxx.com');
//Return-Pathに指定するメールアドレス
define('MAIL_RETURN_PATH', 'info@xxxxxx.com');
//自動返信の返信先名前(自動返信を設定する場合)
define('AUTO_REPLY_NAME', '返信先名前');

また、この例では Bootstrap4 の CSS を読み込んで以下のようなスタイルを指定しています。

/* input 要素 */
#name, #email, #subject, #email_check, #tel {
  max-width:400px;
}
#body {
  max-width: 640px;
}
/* エラー表示 */
p.error, span.error {
  color: red;
}
/* フォーム要素(Bootstrap4 のスタイルを上書き) */
.form-control {
  border-radius: 0px;
  background-color: #fdfdfd;
  font-size: 14px;
}
.form-control:focus {
  border-color: #aadbe8;
  outline: 0;
  -webkit-box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.4);
  box-shadow: inset 0 0px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.4);
  background-color:#fff;
}
/* Google Chrome, Safari, Opera 15+, Android, iOS */
::-webkit-input-placeholder {
  font-size: 13px; 
}
/* Firefox 18- */
:-moz-placeholder {
  font-size: 13px; }
/* Firefox 19+ */
::-moz-placeholder {
  font-size: 13px; }
/* IE 10+ */
:-ms-input-placeholder {
  font-size: 13px; }
::placeholder{ 
  font-size: 13px;
}
textarea.form-control {
  height: 200px;
}
/* 確認ページの表 */
.confirm_table {
  margin: 30px 0; 
}
.confirm_table table caption{
  caption-side: top;
}
.confirm {
  float: left;
  margin-right: 20px;
}

入力ページ

入力ページはユーザーがコンタクトフォーム(contact.php)にアクセスした際に表示されるページです。

入力ページは PHP、HTML、jQuery の3つの部分から構成され、それぞれ以下のような内容になります。

  • PHP : セッションの開始や CSRF 対策の固定トークンの生成、確認ページから戻った際の入力値を表示するための値の初期化など
  • HTML :form 要素を使った入力欄や確認ページへ移動するボタン、トークンの値を設定する隠し要素の設定など
  • jQuery :フォームをサーバへ送信する前の値の検証(オプション)
PHP

以下が入力ページ(contact.php)の PHP の部分です。

最初にセッションを使えるように session_start() 関数を呼び出します。

session_start() 関数は必ず Web ブラウザへの出力が行われる前に呼び出す必要があります。

session_start() 関数を呼び出した後は、セッション変数「$_SESSION」が使えるようになりデータを保存することができます。

そして session_start() の直後に session_regenerate_id() 関数を呼び出してセッション ID を変更します(セッションハイジャック対策)。第1パラメータには必ず「TRUE」を指定します。

12〜18行目は HTML の input 要素の値に出力する変数の初期値の設定です。初回アクセス時は $_SESSION 変数は未定義なので初期値を設定しています。

21〜27行目は HTML でエラーを表示する span 要素に出力する変数の初期値の設定です。$error は配列になっているので、個々のエラーごとに初期値を設定しています。

30〜33行目では初回アクセス時に CSRF 対策の固定トークンを生成して、27行目で生成された固定トークンの値を変数 $ticket に代入しています。

生成されたトークンの値 $ticket は、HTML の隠し属性を指定した input 要素の値に出力され、確認ページに移動する際に検証されるようになっています。

contact.php PHP 部分抜粋
<?php
//セッションを開始
session_start();

//セッションIDを更新して変更(セッションハイジャック対策)
session_regenerate_id( TRUE );

//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//初回以外ですでにセッション変数に値が代入されていれば、その値を。そうでなければNULLで初期化
$name = isset( $_SESSION[ 'name' ] ) ? $_SESSION[ 'name' ] : NULL;
$email = isset( $_SESSION[ 'email' ] ) ? $_SESSION[ 'email' ] : NULL;
$email_check = isset( $_SESSION[ 'email_check' ] ) ? $_SESSION[ 'email_check' ] : NULL;
$tel = isset( $_SESSION[ 'tel' ] ) ? $_SESSION[ 'tel' ] : NULL;
$subject = isset( $_SESSION[ 'subject' ] ) ? $_SESSION[ 'subject' ] : NULL;
$body = isset( $_SESSION[ 'body' ] ) ? $_SESSION[ 'body' ] : NULL;
$error = isset( $_SESSION[ 'error' ] ) ? $_SESSION[ 'error' ] : NULL;

//個々のエラーを初期化($error は定義されていれば配列)
$error_name = isset( $error['name'] ) ? $error['name'] : NULL;
$error_email = isset( $error['email'] ) ? $error['email'] : NULL;
$error_email_check = isset( $error['email_check'] ) ? $error['email_check'] : NULL;
$error_tel = isset( $error['tel'] ) ? $error['tel'] : NULL;
$error_tel_format = isset( $error['tel_format'] ) ? $error['tel_format'] : NULL;
$error_subject = isset( $error['subject'] ) ? $error['subject'] : NULL;
$error_body = isset( $error['body'] ) ? $error['body'] : NULL;

//CSRF対策の固定トークンを生成
if ( !isset( $_SESSION[ 'ticket' ] ) ) {
  //セッション変数にトークンを代入
  $_SESSION[ 'ticket' ] = sha1( uniqid( mt_rand(), TRUE ) );
}

//トークンを変数に代入
$ticket = $_SESSION[ 'ticket' ];
?>
HTML

HTML のフォーム関連の要素を使って入力欄やボタンを表示します。

form 要素の method 属性には post を指定し、action 属性には確認ページ(confirm.php)を指定します。また id 属性を指定し、jQuery のイベントハンドラの設定の際にその id を指定します。

56行目では input 要素に hidden 属性を指定して非表示にし、CSRF 対策用のトークンの値($ticket)を PHP で設定しています。

contact.php HTML 部分抜粋
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム</title>
<link href="../bootstrap.min.css" rel="stylesheet">
<link href="../style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2>お問い合わせフォーム</h2>
  <p>以下のフォームからお問い合わせください。</p>
  <form id="main_contact" method="post" action="confirm.php">
    <div class="form-group">
      <label for="name">お名前(必須) 
        <span class="error"><?php echo h( $error_name ); ?></span>
      </label>
      <input type="text" class="form-control validate max50 required" id="name" name="name" placeholder="氏名" value="<?php echo h($name); ?>">
    </div>
    <div class="form-group">
      <label for="email">Email(必須) 
        <span class="error"><?php echo h( $error_email ); ?></span>
      </label>
      <input type="text" class="form-control validate mail required" id="email" name="email" placeholder="Email アドレス" value="<?php echo h($email); ?>">
    </div>
    <div class="form-group">
      <label for="email_check">Email(確認用 必須) 
        <span class="error"><?php echo h( $error_email_check ); ?></span>
      </label>
      <input type="text" class="form-control validate email_check required" id="email_check" name="email_check" placeholder="Email アドレス(確認のためもう一度ご入力ください。)" value="<?php echo h($email_check); ?>">
    </div>
    <div class="form-group">
      <label for="tel">お電話番号(半角英数字) 
        <span class="error"><?php echo h( $error_tel ); ?></span>
        <span class="error"><?php echo h( $error_tel_format ); ?></span>
      </label>
      <input type="text" class="validate max30 tel form-control" id="tel" name="tel" value="<?php echo h($tel); ?>" placeholder="お電話番号(半角英数字でご入力ください)">
    </div>
    <div class="form-group">
      <label for="subject">件名(必須) 
        <span class="error"><?php echo h( $error_subject ); ?></span> 
      </label>
      <input type="text" class="form-control validate max100 required" id="subject" name="subject" placeholder="件名" value="<?php echo h($subject); ?>">
    </div>
    <div class="form-group">
      <label for="body">お問い合わせ内容(必須) 
        <span class="error"><?php echo h( $error_body ); ?></span>
      </label>
      <span id="count"> </span>/1000
      <textarea class="form-control validate max1000 required" id="body" name="body" placeholder="お問い合わせ内容(1000文字まで)をお書きください" rows="3"><?php echo h($body); ?></textarea>
    </div>
    <button type="submit" class="btn btn-primary">確認画面へ</button>
    <!--確認ページへトークンをPOSTする、隠しフィールド「ticket」-->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
  </form>
</div>
<!--・・・jQuery 部分は省略(後述)・・・-->
</body>
</html>

各入力項目は以下のように div 要素で囲まれた label 要素と input 要素で構成されています。

確認ページの検証で入力内容が条件を満たさないと $error['xxxx'] が設定され、このページに戻されます。

label 要素内には error クラスが付与された span 要素があり、確認ページの検証でエラーが設定された場合はこの部分にエラーメッセージを表示します。

h() は値をエスケープ処理する関数で別途 functions.php で定義されています。

input 要素の値(value 属性)には、確認ページから戻って来た場合などでは一度入力した値が変数に保存されているので echo h($xxxx) の出力により入力欄にエスケープ処理されて表示されます。

<div class="form-group">
  <label for="name">お名前(必須) 
    <span class="error"><?php echo h( $error_name ); ?></span>
  </label>
  <input type="text" class="form-control validate max50 required" id="name" name="name" placeholder="氏名" value="<?php echo h($name); ?>">
</div>

また、input 要素の class 属性には jQuery の検証で使う以下のようなクラスがそれぞれの入力内容に応じて指定されています。

  • validate:検証対象の要素に付与するクラス
  • required:必須項目に付与するクラス
  • max30, max50 :文字の最大数を検証する項目に付与するクラス
  • mail:メールアドレスを検証する項目に付与するクラス
  • tel:電話番号を検証する項目に付与するクラ

.form-control や .form-group は表示(スタイル)を指定するクラスです。

jQuery

このサンプルの場合、確認ページで PHP による入力値の検証を行うので jQuery による以下の検証は必須ではありません(オプションです)。

但し、確認ページでの検証はデータがサーバに送信されてから処理されるため、環境によっては検証結果の表示に時間がかかり使い勝手が良くありません。

そのためこのサンプルでは以下のような jQuery を使った入力値の検証をします。

jQuery の検証は submit イベントに設定して、フォームが送信される際に値を検証します。

もし入力内容に問題があれば、エラーを即座に画面に表示して return false で送信を中断し、修正を促します(70行目)。

jQuery の検証を通過したのに、その後また PHP の検証を行う必要がないように jQuery の検証では PHP による検証と同等かそれ以上の検証を行います。

関連項目:jQuery フォームのバリデーション(検証)

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script> 
<script>
jQuery(function($){
  
  //エラーを表示する関数(error クラスの p 要素を追加して表示)
  function show_error(message, this$) {
    text = this$.parent().find('label').text() + message;
    this$.parent().append("<p class='error'>" + text + "</p>")
  }
  
  //フォームが送信される際のイベントハンドラの設定
  $("#main_contact").submit(function(){
    //エラー表示の初期化
    $("p.error").remove();
    $("div").removeClass("error");
    var text = "";
    $("#errorDispaly").remove();
    
    //メールアドレスの検証
    var email =  $.trim($("#email").val());
    if(email && !(/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/gi).test(email)){
      $("#email").after("<p class='error'>メールアドレスの形式が異なります</p>")
    }
    //確認用メールアドレスの検証
    var email_check =  $.trim($("#email_check").val());
    if(email_check && email_check != $.trim($("input[name="+$("#email_check").attr("name").replace(/^(.+)_check$/, "$1")+"]").val())){
      show_error("が異なります", $("#email_check"));
    }
    //電話番号の検証
    var tel = $.trim($("#tel").val());
    if(tel && !(/^\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}$/gi).test(tel)){
      $("#tel").after("<p class='error'>電話番号の形式が異なります(半角英数字でご入力ください)</p>")
    }
    
    //1行テキスト入力フォームとテキストエリアの検証
    $(":text,textarea").filter(".validate").each(function(){
      //必須項目の検証
      $(this).filter(".required").each(function(){
        if($(this).val()==""){
          show_error(" は必須項目です", $(this));
        }
      })  
      //文字数の検証
      $(this).filter(".max30").each(function(){
        if($(this).val().length > 30){
          show_error(" は30文字以内です", $(this));
        }
      })
      $(this).filter(".max50").each(function(){
        if($(this).val().length > 50){
          show_error(" は50文字以内です", $(this));
        }
      })
      $(this).filter(".max100").each(function(){
        if($(this).val().length > 100){
          show_error(" は100文字以内です", $(this));
        }
      })
      $(this).filter(".max1000").each(function(){
        if($(this).val().length > 1000){
          show_error(" は1000文字以内でお願いします", $(this));
        }
      }) 
    })

    //error クラスの追加の処理
    if($("p.error").length > 0){
      $("p.error").parent().addClass("error");
      $('html,body').animate({ scrollTop: $("p.error:first").offset().top-180 }, 'slow');
      return false;
    }
  }) 
  
  //テキストエリアに入力された文字数を表示
  $("textarea").on('keydown keyup change', function() {
    var count = $(this).val().length;
    $("#count").text(count);
    if(count > 1000) {
      $("#count").css({color: 'red', fontWeight: 'bold'});
    }else{
      $("#count").css({color: '#333', fontWeight: 'normal'});
    }
  });
})
</script>

75〜83行目は、テキストエリアに入力された文字数を表示する記述です。

contact.php コードの全文を別ページで開く

確認ページ

確認ページはユーザーが入力ページでフォームに入力後、「確認画面へ」のボタンをクリックして jQuery の検証及びこのページに記述された PHP の検証を通過した場合に表示されます。

確認ページは PHP、HTML の2つの部分から構成され、それぞれ以下のような内容になります。

  • PHP : 入力ページから POST メソッドで送信されたデータ(入力値)を検証して問題がなければ確認ページを表示し、問題があれば入力ページへ戻す
  • HTML :検証を通過した入力値の表示と、フォームを送信する「送信」ボタン及び入力画面に戻るための「戻る」ボタンを表示
PHP

以下が確認ページ(confirm.php)の PHP の部分です。

セッションを利用するために、session_start() でセッションを開始します。

続いてエスケープ処理やデータチェックを行う関数が記述されているファイル(functions.php)を読み込み、 functions.php で定義されている関数 checkInput() を使って POST されたデータを検証して、もし不正な値が検出されれば die() で処理を中止します(9行目)。

また、入力ページで生成したトークン($_POST[ 'ticket' ])とセッションに保存されているトークン($_SESSION[ 'ticket' ])の値を比較して一致していなければ処理を中止します(16行目)。

24〜29行目では POST されたデータを変数に代入しています(以降の処理でこれらの変数を使うので、値が設定されていなければ NULL を初期値に設定)。

43〜81行目では入力項目の値を検証し、条件に合致しない場合はエラーとして配列 $error に追加します。

84〜90行目では、POST されたデータとエラーメッセージ(の配列)をセッションで使用するために $_SESSION 変数に保存しています。

最後に検証結果にエラーがあれば(配列 $error の要素が1つでもあれば)、header() 関数で入力画面にリダイレクトします(入力画面の URL は $_SERVER 変数で組み立てます)。

エラーがなければ、HTML へ移行して入力内容を表示します。

関連項目:PHP フォームの検証(バリデーション)

confirm.php PHP 部分抜粋
<?php
//セッションを開始
session_start(); 

//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php'; 

//POST されたデータをチェック
$_POST = checkInput( $_POST );

//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access Denied!' );  
  }
} else {
  //トークンが存在しない場合は処理を中止(直接このページにアクセスするとエラーになる)
  die( 'Access Denied(直接このページにはアクセスできません)' );
}

//POSTされたデータを変数に代入
$name = isset( $_POST[ 'name' ] ) ? $_POST[ 'name' ] : NULL;
$email = isset( $_POST[ 'email' ] ) ? $_POST[ 'email' ] : NULL;
$email_check = isset( $_POST[ 'email_check' ] ) ? $_POST[ 'email_check' ] : NULL;
$tel = isset( $_POST[ 'tel' ] ) ? $_POST[ 'tel' ] : NULL;
$subject = isset( $_POST[ 'subject' ] ) ? $_POST[ 'subject' ] : NULL;
$body = isset( $_POST[ 'body' ] ) ? $_POST[ 'body' ] : NULL;

//POSTされたデータを整形(前後にあるホワイトスペースを削除)
$name = trim( $name );
$email = trim( $email );
$email_check = trim( $email_check );
$tel = trim( $tel );
$subject = trim( $subject );
$body = trim( $body );

//エラーメッセージを保存する配列の初期化
$error = array();

//値の検証(入力内容が条件を満たさない場合はエラーメッセージを配列 $error に設定)
if ( $name == '' ) {
  $error['name'] = '*お名前は必須項目です。';
  //制御文字でないことと文字数をチェック
} else if ( preg_match( '/\A[[:^cntrl:]]{1,30}\z/u', $name ) == 0 ) {
  $error['name'] = '*お名前は30文字以内でお願いします。';
}
if ( $email == '' ) {
  $error['email'] = '*メールアドレスは必須です。';
} else { //メールアドレスを正規表現でチェック
  $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD';
  if ( !preg_match( $pattern, $email ) ) {
    $error['email'] = '*メールアドレスの形式が正しくありません。';
  }
}
if ( $email_check == '' ) {
  $error['email_check'] = '*確認用メールアドレスは必須です。';
} else { //メールアドレスを正規表現でチェック
  if ( $email_check !== $email ) {
    $error['email_check'] = '*メールアドレスが一致しません。';
  }
}
if ( preg_match( '/\A[[:^cntrl:]]{0,30}\z/u', $tel ) == 0 ) {
  $error['tel'] = '*電話番号は30文字以内でお願いします。';
}
if ( $tel != '' && preg_match( '/\A\(?\d{2,5}\)?[-(\.\s]{0,2}\d{1,4}[-)\.\s]{0,2}\d{3,4}\z/u', $tel ) == 0 ) {
  $error['tel_format'] = '*電話番号の形式が正しくありません。';
}
if ( $subject == '' ) {
  $error['subject'] = '*件名は必須項目です。';
  //制御文字でないことと文字数をチェック
} else if ( preg_match( '/\A[[:^cntrl:]]{1,100}\z/u', $subject ) == 0 ) {
  $error['subject'] = '*件名は100文字以内でお願いします。';
}
if ( $body == '' ) {
  $error['body'] = '*内容は必須項目です。';
  //制御文字(タブ、復帰、改行を除く)でないことと文字数をチェック
} else if ( preg_match( '/\A[\r\n\t[:^cntrl:]]{1,1050}\z/u', $body ) == 0 ) {
  $error['body'] = '*内容は1000文字以内でお願いします。';
}

//POSTされたデータとエラーの配列をセッション変数に保存
$_SESSION[ 'name' ] = $name;
$_SESSION[ 'email' ] = $email;
$_SESSION[ 'email_check' ] = $email_check;
$_SESSION[ 'tel' ] = $tel;
$_SESSION[ 'subject' ] = $subject;
$_SESSION[ 'body' ] = $body;
$_SESSION[ 'error' ] = $error;

//チェックの結果にエラーがある場合は入力フォームに戻す
if ( count( $error ) > 0 ) {
  //エラーがある場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
} 
?>
HTML

POST されたデータを前述の PHP で変数に代入した値を使って入力内容を表示しています(17行目〜41行目)。出力する際は functions.php に定義されている h() 関数でエスケープ処理します。

変数 $body(お問い合わせ内容)の出力では、改行を表示するため nl2br() 関数を使用しています。

42〜44行目は入力ページへ戻るためのボタンを表示するフォーム要素で action 属性に contact.php(入力ページ)を指定します。ユーザーが入力した値はすでにセッション変数に入っているため、それらの値を POST する必要はありません。

45〜53行目は「送信ボタン」を表示するフォーム要素で、action 属性に complete.php(完了ページ)を指定します。こちらのフォームでは CSRF対策のトークンの値($ticket)を POST する必要があります。

confirm.php HTML 部分抜粋
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム(確認)</title>
<link href="../bootstrap.min.css" rel="stylesheet">
<link href="../style.css" rel="stylesheet">
</head>
</head>
<body>
<div class="container">
  <h2>お問い合わせ確認画面</h2>
  <p>以下の内容でよろしければ「送信する」をクリックしてください。<br>
    内容を変更する場合は「戻る」をクリックして入力画面にお戻りください。</p>
  <div class="table-responsive">
    <table class="table table-bordered">
      <caption>ご入力内容</caption>
      <tr>
        <th>お名前</th>
        <td><?php echo h($name); ?></td>
      </tr>
      <tr>
        <th>Email</th>
        <td><?php echo h($email); ?></td>
      </tr>
      <tr>
        <th>お電話番号</th>
        <td><?php echo h($tel); ?></td>
      </tr>
      <tr>
        <th>件名</th>
        <td><?php echo h($subject); ?></td>
      </tr>
      <tr>
        <th>お問い合わせ内容</th>
        <td><?php echo nl2br(h($body)); ?></td>
      </tr>
    </table>
  </div>
  <form action="contact.php" method="post" class="confirm">
    <button type="submit" class="btn btn-secondary">戻る</button>
  </form>
  <form action="complete.php" method="post" class="confirm">
    <!-- 完了ページへ渡すトークンの隠しフィールド -->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button type="submit" class="btn btn-success">送信する</button>
  </form>
</div>
</body>
</html>

comfirm.php コードの全文を別ページで開く

完了ページ

ユーザーが確認ページで、「送信する」のボタンをクリックすると form 要素の action 属性に指定されている complete.php に method 属性で指定されている post メソッドで送信されます。

完了ページでは、CSRF 対策の固定トークンの検証をして問題がなければユーザの入力した値を使ってメール本文を作成してメール送信の処理を行います。

PHP

以下が確認ページ(complete.php)の PHP の部分です。

セッションを利用するために、session_start() でセッションを開始し、続いてエスケープ処理やデータチェックを行う関数が記述されているファイル(functions.php)とメールアドレスなどを記述してある mailvars.php を読み込みます。

10行目はメール本文にお問い合わせ日時を日本時間で記載するためにタイムゾーンを設定しています。

そして functions.php で定義されている関数 checkInput() を使って POST されたデータを検証して、もし不正な値が検出されれば die() で処理を中止します(13行目)。

16行目では確認ページの HTML で input 要素に指定した値($ticket)とセッションに保存されているトークン($_SESSION[ 'ticket' ])の値を比較して一致していなければ処理を中止します。

トークンが存在しない場合も処理を中断しても良いのですが、完了ページでページを再読み込みした場合を考慮して入力ページにリダイレクトしています。

但し、header() 関数実行後は exit を記述しないと、以降の処理が実行されてしまうため再読み込みの際に値のないメールが送信されてしまいます(22〜31行目)。

33〜46行目は入力された値をエスケープ処理してそれらをもとにメール本文を作成しています。

51〜70行目は mb_send_mail() を使ったメールの送信処理です。

mb_send_mail() はメールの送信が成功した場合は true を、失敗した場合は false を戻り値として返すので $result に代入しておきます。

メールの送信が成功した場合はすべてのセッション変数を消去し、session_destroy() でセッションを破棄します。

また、メールの送信結果($result)をもとに HTML ではメッセージ(成功または失敗)を表示します。

complete.php PHP 部分抜粋
<?php
//セッションを開始
session_start(); 
//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php'; 
//メールアドレス等を記述したファイルの読み込み
require '../libs/mailvars.php'; 

//お問い合わせ日時を日本時間に
date_default_timezone_set('Asia/Tokyo'); 

//POSTされたデータをチェック
$_POST = checkInput( $_POST );

//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' ); //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit; //忘れないように
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

//メール本文の組み立て
$mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
$mail_body .=  date("Y年m月d日 H時i分") . "\n\n"; 
$mail_body .=  "お名前: " .$name . "\n";
$mail_body .=  "Email: " . $email . "\n"  ;
$mail_body .=  "お電話番号: " . $tel . "\n\n" ;
$mail_body .=  "<お問い合わせ内容>" . "\n" . $body;
  
//-------- sendmail(mb_send_mail)を使ったメールの送信処理------------

//メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
$mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

//Return-Pathに指定するメールアドレス
$returnMail = MAIL_RETURN_PATH; //
//mbstringの日本語設定
mb_language( 'ja' );
mb_internal_encoding( 'UTF-8' );

// 送信者情報(From ヘッダー)の設定
$header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
$header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
$header .= "Bcc: <" . MAIL_BCC.">";

//メールの送信(結果を変数 $result に代入)
if ( ini_get( 'safe_mode' ) ) {
  //セーフモードがOnの場合は第5引数が使えない
  $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
} else {
  $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
HTML

メールの送信処理の結果をユーザに表示します。

メールの送信処理が完了すると $result に結果の真偽値が入っているので、その値で表示する内容を切り替えています。

送信が成功した場合は15〜17行目が表示され、失敗した場合は19〜21行目が表示されます。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>コンタクトフォーム(完了)</title>
<link href="../bootstrap.min.css" rel="stylesheet">
<link href="../style.css" rel="stylesheet">
</head>
<body>
<div class="container">
  <h2>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>
</html>

complete.php コードの全文を別ページで開く

reCAPTCHA v3 を使う

前述のコンタクトフォームに reCAPTCHA v3 を実装する例です。

確認ページの画面右下に reCAPTCHA v3 のウィジェットが表示されます。また、このサンプルでは確認用に完了ページにスコアなどの判定結果を表示します。

サンプルを別ページで開く

関連ページ:Google reCAPTCHA の使い方(v2/v3)

入力ページ

この例では reCAPTCHA v3 は確認ページに実装して「送信」ボタンをクリックした際に reCAPTCHA のトークンを生成して送信します。

そのため、入力ページは前述のコンタクトフォームの入力ページ(contact.php)と同じです。

入力ページに reCAPTCHA のウィジェットを表示するには、ウィジェットを表示する API にサイトキーを指定して読み込みます。

API の読み込み
<script src="https://www.google.com/recaptcha/api.js?render=サイトキー"></script> 

contact.php コードの全文を別ページで開く

確認ページ

確認ページの「送信」ボタンをクリックした際に reCAPTCHA のトークンを生成するようにします。

reCAPTCHA を使用するためのサイトキーとシークレットキーは mailvars.php などのファイルを保存してある外部からアクセスできないフォルダ libs に recaptchavars.php という名前で保存します(mailvars.php に記述することもできますが)。

recaptchavars.php
<?php
// reCAPTCHA v3 サイトキー
define('V3_SITEKEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx');
// reCAPTCHA v3 シークレットキー
define('V3_SECRETKEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxx');
PHP

確認ページ(confirm.php)の PHP ではサイトキーとシークレットキーを記述したファイル(recaptchavars.php)を読み込み、サイトキーを変数($siteKey)に代入します。

変数 $siteKey は reCAPTCHA の API の読み込み及びトークンの取得で使用します。

confirm.php PHP 一部抜粋
<?php
//セッションを開始
session_start();
//エスケープ処理やデータチェックを行う関数のファイルの読み込み
require '../libs/functions.php';

//reCAPTCHA サイトキーを記述したファイルの読み込み(★追加)
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー(★追加)
$siteKey = V3_SITEKEY;

・・・以下省略(以下は前述のコンタクトフォームの confirm.php と全く同じ)
HTML

jQuery で送信するフォームを指定しやすいように「送信」ボタンを記述してある form 要素に id(この例では #complete)を指定します(23行目)。

また、render パラメータにサイトキーを指定して reCAPTCHA の API を読み込みます(29行目)。

confirm.php HTML 一部抜粋
<body>
<div class="container">
  <h2>お問い合わせ確認画面</h2>
  <p>以下の内容でよろしければ「送信する」をクリックしてください。<br>
    内容を変更する場合は「戻る」をクリックして入力画面にお戻りください。</p>
  <div class="table-responsive confirm_table">
    <table class="table table-bordered" style="max-width:600px;">
      <caption>ご入力内容</caption>
      <tr>
        <th>お名前</th>
        <td><?php echo h($name); ?></td>
      </tr>
      ・・・中略・・・
      <tr>
        <th>お問い合わせ内容</th>
        <td><?php echo nl2br(h($body)); ?></td>
      </tr>
    </table>
  </div>
  <form action="contact.php" method="post" class="confirm">
    <button type="submit" class="btn btn-secondary">戻る</button>
  </form>
  <form id="complete" action="complete.php" method="post" class="confirm">
    <!-- 完了ページへ渡すトークンの隠しフィールド -->
    <input type="hidden" name="ticket" value="<?php echo h($ticket); ?>">
    <button type="submit" class="btn btn-success">送信する</button>
  </form>
</div>
<script src="https://www.google.com/recaptcha/api.js?render=<?php echo $siteKey; ?>"></script><!-- reCAPTCHA v3 の読み込み(★追加) -->
jQuery

「送信」ボタンをクリックした際に reCAPTCHA のトークンを生成するように以下を記述します。

1行目は jQuery の読み込みです。

送信ボタンを設定した form 要素(#complete)の submit イベントハンドラでトークンを取得して complete.php へ送信します。

submit イベントハンドラでは event.preventDefault() で送信を一時中断し、hidden 属性を指定した input 要素を作成して、パラメータのトークン(token)とアクション名を値に設定しています(12〜13行目)。

関連ページ(項目):reCAPTCHA v3 クライアント側の実装

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script><!-- jQuery の読み込み(★追加) -->  
<script>
//reCAPTCHA v3 トークン取得(★追加)
jQuery(function($){
  $("#complete").submit(function(event){
    var that = $(this);
    event.preventDefault();
    var action_name = 'contact'; //アクション名 
    grecaptcha.ready(function() {
      grecaptcha.execute('<?php echo $siteKey; ?>', { action: action_name }).then(function(token) {
        //input 要素を生成して値にトークンを設定
        that.prepend('<input type="hidden" name="g-recaptcha-response" value="' + token + '">');
        //input 要素を生成して値にアクション名を設定
        that.prepend('<input type="hidden" name="action" value="' + action_name + '">');
        //unbind で一度 submit のイベントハンドラを削除してから submit() を実行
        that.unbind('submit').submit(); 
      });;
    });
  });
})
</script>

confirm.php コードの全文を別ページで開く

完了ページ

完了ページでは、確認ページから送信された reCAPTCHA のトークンとアクション名を reCAPTCHA の API を使って検証します。

そして検証結果に問題がなければメールの送信処理を実行します。

PHP

サイトキーとシークレットキーを変数($siteKey と $secretKey)に代入しておきます。

確認ページから送信された reCAPTCHA のトークンとアクション名を変数に代入し、reCAPTCHA の検証結果を格納する変数($rcv3_result)に初期値を設定します(35〜40行目)。

reCAPTCHA のトークンとアクション名が取得できていればそれらの値を使って API からレスポンスを取得して判定し、判定結果を変数($rcv3_result)に代入します(43〜75行目)。

以下の例では判定の基準としてスコアの値が0.5以上であれば合格としていますが、この値を変更することで判定を調整することができます。

そして判定結果がOK(合格)であれば、メールを送信します。

関連ページ(項目):reCAPTCHA v3 PHP を使った検証

<?php
//セッションを開始
session_start();
//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php';
//メールアドレス等を記述したファイルの読み込み
require '../libs/mailvars.php';

//reCAPTCHA サイトキーを記述したファイルの読み込み(★追加)
require '../libs/recaptchavars.php';
// reCAPTCHA サイトキー(★追加)
$siteKey = V3_SITEKEY;
// reCAPTCHA シークレットキー(★追加)
$secretKey = V3_SECRETKEY;

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}

//reCAPTCHA トークン(★追加)
$token = isset( $_POST[ 'g-recaptcha-response' ] ) ? $_POST[ 'g-recaptcha-response' ] : NULL;
//reCAPTCHA アクション名 (★追加)
$action = isset( $_POST[ 'action' ] ) ? $_POST[ 'action' ] : NULL;
//reCAPTCHA の検証を通過したかどうかの真偽値(★追加)
$rcv3_result = false;

// reCAPTCHA のトークンとアクション名が取得できていれば(★追加)
if ( $token && $action ) {

  //cURL セッションを初期化(API のレスポンスの取得)
  $ch = curl_init();
  // curl_setopt() により転送時のオプションを設定
  //URL の指定
  curl_setopt( $ch, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify" );
  //HTTP POST メソッドを使う
  curl_setopt( $ch, CURLOPT_POST, true );
  //API パラメータの指定
  curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( array(
    'secret' => $secretKey,
    'response' => $token
  ) ) );
  //curl_execの返り値を文字列にする
  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
  //転送を実行してレスポンスを $api_response に代入
  $api_response = curl_exec( $ch );
  //セッションを終了
  curl_close( $ch );

  //レスポンスの $json(JSON形式)をデコード
  $rc_result = json_decode( $api_response );

  //レスポンスの値を判定
  if ( $rc_result->success && $rc_result->action === $action && $rc_result->score >= 0.5 ) {
    //success が true でアクション名が一致し、スコアが 0.5 以上の場合は合格
    $rcv3_result = true;
  } else {
    // 上記以外の場合は 不合格
    $rcv3_result = false;
  }
}

//メールの送信結果の初期値を false に
$result = false;

//reCAPTCHA の検証結果が合格の場合はメール送信処理を実行
if ( $rcv3_result ) { //(★検証結果の判定を追加)
  //メールの送信処理(省略)
}

//メール送信の結果で分岐
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>
HTML

HTML では送信結果(成功または失敗)により表示を切り替えていて、前述のコンタクトフォームの場合と同じです。

サンプルでは以下のような reCAPTCHA の判定結果の確認(テスト)用の表示を記述しています(上記のサンプルでは「reCAPTCHA API レスポンス」部分はコメントアウトしています)。

<?php if (isset($rc_result )): ?>
<h4 style="margin: 20px 0;">reCAPTCHA 判定結果表示</h4>
<ul>
  <li><?php echo 'success 判定 :' . $rc_result->success; ?></li>
  <li><?php echo 'アクション名 : ' . $rc_result->action ?></li>
  <li><?php echo 'スコア : ' . $rc_result->score; ?></li>
</ul>
<h4 style="margin: 20px 0;">reCAPTCHA API レスポンス</h4>
<pre><?php var_dump($rc_result ); ?></pre>
<?php endif; ?>

complete.php コードの全文を別ページで開く

自動返信

ユーザがコンタクトフォームを送信して成功したら、ユーザに確認のメールを自動的に返信する例です。

メールの送信処理の部分に自動返信の記述を追加します。入力ページや確認ページに変更はありません。

18〜23行目でメールの送信処理を行って、メールの送信が成功すると $result は true になるので、その場合は26行目以降が実行されます。

自動返信も mb_send_mail() を使って送信します(51〜55行目)。

また、$show_autoresponse_msg という変数を使って自動返信の結果を画面に表示するかどうかを切り替えられるようにしています。

complete.php メール送信部分抜粋
//-------- sendmail(mb_send_mail)を使ったメールの送信処理------------

//メールの宛先(名前<メールアドレス> の形式)。値は mailvars.php に記載
$mailTo = mb_encode_mimeheader(MAIL_TO_NAME) ."<" . MAIL_TO. ">";

//Return-Pathに指定するメールアドレス
$returnMail = MAIL_RETURN_PATH; //
//mbstringの日本語設定
mb_language( 'ja' );
mb_internal_encoding( 'UTF-8' );

// 送信者情報(From ヘッダー)の設定
$header = "From: " . mb_encode_mimeheader($name) ."<" . $email. ">\n";
$header .= "Cc: " . mb_encode_mimeheader(MAIL_CC_NAME) ."<" . MAIL_CC.">\n";
$header .= "Bcc: <" . MAIL_BCC.">";

//メールの送信(結果を変数 $result に格納)
if ( ini_get( 'safe_mode' ) ) {
  //セーフモードがOnの場合は第5引数が使えない
  $result = mb_send_mail( $mailTo, $subject, $mail_body, $header );
} else {
  $result = mb_send_mail( $mailTo, $subject, $mail_body, $header, '-f' . $returnMail );
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy(); //セッションを破棄
  
  //自動返信メールの送信処理
  //自動返信メールの送信が成功したかどうかのメッセージを表示する場合は true
  $show_autoresponse_msg = true;
  //ヘッダー情報
  $ar_header = "MIME-Version: 1.0\n";
  $ar_header .= "From: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
  $ar_header .= "Reply-To: " . mb_encode_mimeheader( AUTO_REPLY_NAME ) . " <" . MAIL_TO . ">\n";
  //件名
  $ar_subject = 'お問い合わせ自動返信メール';
  //本文
  $ar_body = $name." 様\n\n";
  $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
  $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
  $ar_body .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
  $ar_body .= "お名前:" . $name . "\n";
  $ar_body .= "メールアドレス:" . $email . "\n";
  $ar_body .= "お電話番号: " . $tel . "\n\n" ;
  $ar_body .="<お問い合わせ内容>" . "\n" . $body;
  
  //自動返信の送信(結果を変数 result2 に格納)
  if ( ini_get( 'safe_mode' ) ) {
    $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header  );
  } else {
    $result2 = mb_send_mail( $email, $ar_subject, $ar_body , $ar_header , '-f' . $returnMail );
  }
} else {
  //送信失敗時(もしあれば)
}

以下は送信結果を表示する部分の HTML です。

8〜14行目が自動返信メールの送信結果の表示部分です。この表示が不要な場合は PHP の $show_autoresponse_msg を false に設定します。

complete.php 一部抜粋
<body>
<div class="container">
  <h2>お問い合わせフォーム</h2>
  <?php if ( $result ): ?>
  <h3>送信完了!</h3>
  <p>お問い合わせいただきありがとうございます。</p>
  <p>送信完了いたしました。</p>
    <?php if ( $show_autoresponse_msg ): ?>
      <?php if ( $result2 ): ?>
      <p>確認の自動返信メールを <?php echo $email; ?> へお送りいたしました。</p>
      <?php else: ?>
      <p>確認の自動返信メールを送信できませんでした。</p>
      <?php endif; ?>
    <?php endif; ?>
  <?php else: ?>
  <p>申し訳ございませんが、送信に失敗しました。</p>
  <p>しばらくしてもう一度お試しになるか、メールにてご連絡ください。</p>
  <p>ご迷惑をおかけして誠に申し訳ございません。</p>
  <?php endif; ?>
</div>
</body>

complete.php コードの全文を別ページで開く

PHPMailer を使う

mb_send_mail() の代わりに PHP のライブラリ PHPMailer を使ってメールを送信する例です。

PHPMailer を別途インストールしておきます。また、メールサーバやメールアカウントのパスワードなどの情報を記述したファイル(phpmailvars.php)を作成し、外部からアクセスできないようにします。

以下がフォルダ構成です。この例の場合、HPMailer は Composer を使ってインストールしています。

関連ページ:PHPMailer の使い方

├── contact
│   ├── contact.php  //入力ページ
│   ├── confirm.php  //確認ページ
│   └── complete.php //完了ページ
├── libs
│   ├── .htaccess //アクセス制御(このフォルダへの外部からのアクセスを拒否)
│   ├── functions.php //値を検証する関数やエスケープ処理をする関数のファイル
│   └── phpmailvars.php //メールアカウントのパスワードなどの情報を記述したファイル
├── php_mailer //PHPMailer
│   ├── composer.json
│   ├── composer.lock
│   └── vendor
│       ├── autoload.php
│       └── composer
│
└── style.css //スタイルシート
phpmailvars.php
<?php
//SMTP サーバー(サーバーの場合:localhost でも可)
define('MAIL_HOST', 'mail.xxxxxx.com');
 
//PHPMailer を使って送信するための E-mail アカウント
define('MAIL_USER', 'xxxxx@xxxxxxx.com');
 
//パスワード
define('MAIL_PASSWORD', 'xxxxxxxxxx');
 
//送信先
define('SEND_TO', 'xxxx@xxxxxx.com');
 
//送信先の名前
define('SEND_TO_NAME', '送信先名前');
 
//Bcc アドレス
define('BCC', 'xxxx@xxxxxx.com');
 
//自動返信をする場合の送信元アドレス
define('AR_SEND_FROM', 'xxxx@xxxxxxxx.com');
 
//自動返信をする場合の送信元名前
define('AR_SEND_FROM_NAME', '自動返信送信元名前');

以下が完了ページの PHP 部分で、メールの送信処理を PHPMailer で行っています。

6行目は PHPMailer の読み込みです。Composer を使ってインストールしているので、Composer が提供する autoload.php を読み込んでいます。

メールの送信処理では PHPMailer のインスタンスを生成し(63行目)、そのプロパティやメソッドを使って送信処理の記述をします。

67行目はメインテナンス(デバグ)用の記述で、コメントアウトを外すとデバグの結果が全て出力されます。うまく送信できない場合などに利用します。

96行目のコメントアウトを外すと送信ができなかった際に PHPMailer によるエラーメッセージが表示されます。

complete.php PHP 部分抜粋
<?php
//セッションを開始
session_start(); 

//PHPMailer の読み込み (★PHPMailer 用に追加)
require '../php_mailer/vendor/autoload.php';
//メールアカウント情報(パスワード等)の読み込み (★PHPMailer 用に追加)
require '../libs/phpmailvars.php'; 

//エスケープ処理やデータをチェックする関数を記述したファイルの読み込み
require '../libs/functions.php'; 

//お問い合わせ日時を日本時間に
date_default_timezone_set('Asia/Tokyo'); 

//POSTされたデータをチェック
$_POST = checkInput( $_POST );
//固定トークンを確認(CSRF対策)
if ( isset( $_POST[ 'ticket' ], $_SESSION[ 'ticket' ] ) ) {
  $ticket = $_POST[ 'ticket' ];
  if ( $ticket !== $_SESSION[ 'ticket' ] ) {
    //トークンが一致しない場合は処理を中止
    die( 'Access denied' );
  }
} else {
  //トークンが存在しない場合(入力ページにリダイレクト)
  //die( 'Access Denied(直接このページにはアクセスできません)' );  //処理を中止する場合
  $dirname = dirname( $_SERVER[ 'SCRIPT_NAME' ] );
  $dirname = $dirname == DIRECTORY_SEPARATOR ? '' : $dirname;
  $url = ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http://' : 'https://' ) . $_SERVER[ 'SERVER_NAME' ] . $dirname . '/contact.php';
  header( 'HTTP/1.1 303 See Other' );
  header( 'location: ' . $url );
  exit;
}

//変数にエスケープ処理したセッション変数の値を代入
$name = h( $_SESSION[ 'name' ] );
$email = h( $_SESSION[ 'email' ] ) ;
$tel =  h( $_SESSION[ 'tel' ] ) ;
$subject = h( $_SESSION[ 'subject' ] );
$body = h( $_SESSION[ 'body' ] );

//メール本文の組み立て
$mail_body = 'コンタクトページからのお問い合わせ' . "\n\n";
$mail_body .=  date("Y年m月d日 H時i分") . "\n\n"; 
$mail_body .=  "お名前: " .$name . "\n";
$mail_body .=  "Email: " . $email . "\n"  ;
$mail_body .=  "お電話番号: " . $tel . "\n\n" ;
$mail_body .=  "<お問い合わせ内容>" . "\n" . $body;
  
//-------- ★★★ PHPMailer を使ったメールの送信処理 ★★★ ------------

//PHPMailer 名前空間の使用
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

//mbstring の日本語設定
mb_language("japanese");
mb_internal_encoding("UTF-8");

//PHPMailer のインスタンスを生成 
$mail = new PHPMailer(true);

try {
  //サーバ設定
  //$mail->SMTPDebug = SMTP::DEBUG_SERVER;   // デバグの出力を有効に
  $mail->isSMTP();  // SMTP を使用
  $mail->Host       = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
  $mail->SMTPAuth   = true;      // SMTP authentication を有効に
  $mail->Username   = MAIL_USER; // SMTP ユーザ名(phpmailvars.phpで定義)
  $mail->Password   = MAIL_PASSWORD; // SMTP パスワード(phpmailvars.phpで定義)
  $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // TLS を有効に
  $mail->Port       = 587; // TCP ポートを指定 
  
  //日本語用
  $mail->CharSet = "iso-2022-jp";
  $mail->Encoding = "7bit";
  
  //Recipients
  $mail->setFrom($email, mb_encode_mimeheader($name));  //差出人アドレス, 差出人名 
  $mail->AddAddress(SEND_TO, mb_encode_mimeheader(SEND_TO_NAME)); //送信先アドレス・宛先名(phpmailvars.phpで定義)
  $mail->AddBcc(BCC);  //Bcc アドレス(phpmailvars.phpで定義) 

  $mail->isHTML(false);    // Set email format to plain text
  $mail->Subject = mb_encode_mimeheader($subject);   //件名
  $mail->WordWrap = 70;  //70 文字で改行(好みで)
  
  $mail->Body  = mb_convert_encoding($mail_body,"JIS","UTF-8");

  //メール送信の結果(真偽値)を $result に代入
  $result = $mail->send();

} catch (Exception $e) {
  //PHPMailer のエラーを表示する場合
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy(); //セッションを破棄
} else {
  //送信失敗時(もしあれば)
}
?>

complete.php コードの全文を別ページで開く

自動返信あり

以下は前述の例に自動返信の記述を追加した例です。

自動返信を追加する場合、新たに自動返信用のインスタンスを生成してそのメソッドやプロパティを使います(57行目〜)。

complete.php メール送信及び自動返信部分抜粋
//-------- ★★★ PHPMailer を使ったメールの送信処理 ★★★ ------------

//PHPMailer 名前空間の使用(★PHPMailer 用に追加)
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

//mbstring の日本語設定
mb_language("japanese");
mb_internal_encoding("UTF-8");

//PHPMailer のインスタンスを生成 
$mail = new PHPMailer(true);

try {
  //サーバ設定
  //$mail->SMTPDebug = SMTP::DEBUG_SERVER;   // デバグの出力を有効に
  $mail->isSMTP();  // SMTP を使用
  $mail->Host       = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
  $mail->SMTPAuth   = true;      // SMTP authentication を有効に
  $mail->Username   = MAIL_USER; // SMTP ユーザ名(phpmailvars.phpで定義)
  $mail->Password   = MAIL_PASSWORD; // SMTP パスワード(phpmailvars.phpで定義)
  $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // TLS を有効に
  $mail->Port       = 587; // TCP ポートを指定 
  
  //日本語用
  $mail->CharSet = "iso-2022-jp";
  $mail->Encoding = "7bit";
  
  //Recipients
  $mail->setFrom($email, mb_encode_mimeheader($name));  //差出人アドレス, 差出人名 
  $mail->AddAddress(SEND_TO, mb_encode_mimeheader(SEND_TO_NAME)); //送信先アドレス・宛先名(phpmailvars.phpで定義)
  $mail->AddBcc(BCC);  //Bcc アドレス(phpmailvars.phpで定義) 

  $mail->isHTML(false);    // Set email format to plain text
  $mail->Subject = mb_encode_mimeheader($subject);   //件名
  $mail->WordWrap = 70;  //70 文字で改行(好みで)
  
  $mail->Body  = mb_convert_encoding($mail_body,"JIS","UTF-8");

  //メール送信の結果(真偽値)を $result に代入
  $result = $mail->send();

} catch (Exception $e) {
  //PHPMailer のエラーを表示する場合
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

//メール送信の結果判定
if ( $result ) {
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy(); //セッションを破棄

  //★★★ 自動返信メール ★★★ 
  //自動返信のインスタンスを生成
  $autoresponder = new PHPMailer( true );
  try {
    //サーバ設定
    //$autoresponder->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
    $autoresponder->isSMTP(); // SMTP を使用
    $autoresponder->Host = MAIL_HOST; // SMTP サーバーを指定(phpmailvars.phpで定義)
    $autoresponder->SMTPAuth = true; // SMTP authentication を有効に
    $autoresponder->Username = MAIL_USER; // SMTP ユーザ名(phpmailvars.phpで定義)
    $autoresponder->Password = MAIL_PASSWORD; // SMTP パスワード(phpmailvars.phpで定義)
    $autoresponder->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // TLS を有効に
    $autoresponder->Port = 587; // TCP ポートを指定 

    //日本語用
    $autoresponder->CharSet = "iso-2022-jp";
    $autoresponder->Encoding = "7bit";

    //Recipients
    $autoresponder->setFrom( AR_SEND_FROM, mb_encode_mimeheader( AR_SEND_FROM_NAME ) ); //差出人アドレス, 差出人名 
    $autoresponder->AddAddress( $email, mb_encode_mimeheader( $name ) ); //送信先・宛先(phpmailvars.phpで定義) 

    $autoresponder->isHTML( false ); // テキストメール
    $autoresponder->Subject = mb_encode_mimeheader( "自動返信メール" ); //件名
    //返信用アドレス(差出人以外に別途指定する場合)
    $autoresponder->addReplyTo( MAIL_USER, mb_encode_mimeheader("お問い合わせ")); 
    $autoresponder->WordWrap = 70; //70 文字で改行(好みで)
    $ar_body = $name." 様\n\n";
    $ar_body .= "この度は、お問い合わせ頂き誠にありがとうございます。" . "\n\n";
    $ar_body .= "下記の内容でお問い合わせを受け付けました。\n\n";
    $ar_body .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
    $ar_body .= "お名前:" . $name . "\n";
    $ar_body .= "メールアドレス:" . $email . "\n";
    $ar_body .= "お電話番号: " . $tel . "\n\n" ;
    $ar_body .="<お問い合わせ内容>" . "\n" . $body;
    $autoresponder->Body = mb_convert_encoding( $ar_body, "JIS", "UTF-8" );
    //自動送信メールの送信結果(真偽値)を result2 に代入
    $result2 = $autoresponder->send();
  } catch ( Exception $e ) {
    //PHPMailer のエラーを表示する場合
    //echo "Auto Response Message could not be sent. Mailer Error: {$autoresponder->ErrorInfo}";
  }
} else {
  //送信失敗時(もしあれば)
}

complete.php コードの全文を別ページで開く

Gmail の SMTP サーバを使用

以下は Gmail の SMTP 認証で XOAUTH2 を使う例です。

XOAUTH2 を使えば、「安全性の低いアプリの許可」を有効にせずに Gmail の SMTP サーバを使用することができます。

但し、XOAUTH2 のライブラリを使用するには PHPMailer 以外に oauth2-client(oauth2-google)のインストールとGoogle API プロジェクトの登録(割と手間がかかります)が必要です。

関連ページ:Gmail の SMTP 認証で XOAUTH2 を使う

以下の例の場合、前述の PHPMailer を使う例とフォルダ構成が異なります。

├── contact_Gmail  //メインのフォルダ
│   ├── complete.php  //完了ページ
│   ├── confirm.php   //確認ページ
│   ├── contact.php   //入力ページ
│   └── phpmailer_oauth2 //PHPMailer と依存ライブラリ
│       ├── composer.json
│       ├── composer.lock
│       └── vendor
│           ├── autoload.php
│           ├── composer
│           ├── guzzlehttp
│           ├── league
│           ・・・以下省略・・・
├── libs
│   ├── .htaccess //アクセス制御(このフォルダへの外部からのアクセスを拒否)
│   ├── functions.php
│   └── phpmailvars_oauth2.php //Gmailのアカウントの情報
└── style.css //スタイルシート

phpmailvars_oauth2.php
<?php
//SMTP サーバの指定(Gmail)
define('MAIL_HOST', 'smtp.gmail.com');
//Gmail アドレス(この例の場合は、送信元と送信先、及び自動送信の送信元にこのアドレスを使用)
define('GMAIL_ADDRESS', 'xxxxxx@gmail.com');
//Gmail のアカウント名
define('GMAIL_ACCT_NAME', '〇〇〇〇');
//クライアント ID
define('CLIENT_ID', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
//クライアントシークレット
define('CLIENT_SECRET', 'xxxxxxxxxxxxxxxxxxxx');
//トークン
define('REFRESH_TOKEN', 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
//Bcc
define('BCC', 'xxxxxx@xxxxxx.com');

以下は PHPMailer を使って Gmail のアカウントでメールを送信する例です。

complete.php Gmail SMTP でのメール送信部分抜粋
//--- PHPMailer を使ったメールの送信処理(Gmailサーバを使用する場合) -----
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\OAuth; //  ### 追加 ### 
 
// Alias the League Google OAuth2 provider class
use League\OAuth2\Client\Provider\Google;//  ### 追加 ### 

//PHPMailer(Composer のオートローダ)の読み込み
require 'phpmailer_oauth2/vendor/autoload.php';
//メールアカウント情報(パスワード等)の読み込み
require '../libs/phpmailvars_oauth2.php';

//mbstring の日本語設定
mb_language( "japanese" );
mb_internal_encoding( "UTF-8" );

// ###  OAUTH2 の設定  ### 
//Gmail メールアドレス(phpmailvars_oauth2.php で定義)
$google_email = GMAIL_ADDRESS;
//クライアント ID(phpmailvars_oauth2.php で定義)
$clientId = CLIENT_ID;
//クライアントシークレット(phpmailvars_oauth2.php で定義)
$clientSecret = CLIENT_SECRET;
//トークン(phpmailvars_oauth2.php で定義)
$refreshToken = REFRESH_TOKEN;

//PHPMailer のインスタンスを生成 
$mail = new PHPMailer( true );

// ###  OAUTH2 の設定  ### 
//OAuth2 プロバイダのインスタンスの生成 
$provider = new Google(
  [
    'clientId' => $clientId,
    'clientSecret' => $clientSecret,
  ]
);
//送信結果の真偽値の初期化
$result = false;
$result2 = false;

try {
  //サーバ設定
  //$mail->SMTPDebug = SMTP::DEBUG_SERVER; // デバグの出力を有効に
  $mail->isSMTP(); // SMTP を使用
  // SMTP サーバーを指定
  $mail->Host = MAIL_HOST; 
  // SMTP authentication を有効に
  $mail->SMTPAuth = true; 
  //AuthType を XOAUTH2 に指定
  $mail->AuthType = 'XOAUTH2'; 
  // 暗号化(TLS)を有効に
  $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
  //ポートの指定
  $mail->Port = 587;

  //日本語用
  $mail->CharSet = "iso-2022-jp";
  $mail->Encoding = "7bit";
  
  // ###  OAUTH2 の設定  ### 
  //OAuth プロバイダのインスタンスを PHPMailer へ渡す
  $mail->setOAuth(
    new OAuth(
      [
        'provider' => $provider,
        'clientId' => $clientId,
        'clientSecret' => $clientSecret,
        'refreshToken' => $refreshToken,
        'userName' => $google_email,
      ]
    )
  );

  //受信者設定
  //差出人アドレス, 差出人名(差出人アドレスには Gmail アカウントのアドレスを指定)
  $mail->setFrom($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME));  
  //送信先アドレス (この例の場合は Gmail のアドレス)・宛先名
  $mail->AddAddress($google_email, mb_encode_mimeheader(GMAIL_ACCT_NAME)); 
  //返信アドレスに差出人(お問い合わせをしたユーザ)を指定
  $mail->addReplyTo($email, mb_encode_mimeheader($name)); 
  //Bcc アドレス
  $mail->AddBcc( BCC );  
  // テキスト形式メール
  $mail->isHTML( false );
  //件名
  $mail->Subject = mb_encode_mimeheader( $subject ); 
  //70 文字で改行(好みで)
  $mail->WordWrap = 70; 
  //本文
  $mail->Body = mb_convert_encoding( $mail_body, "JIS", "UTF-8" );

  //メール送信の結果(真偽値)を $result に代入
  $result = $mail->send();

} catch ( Exception $e ) {
  //echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}

//メール送信の結果($result) を判定
if ( $result ) {
  
  //成功した場合はセッションを破棄
  $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 
  session_destroy(); //セッションを破棄

  //自動返信メール
  //自動返信用のインスタンスの生成
  $autoresponder = new PHPMailer( true );
  ・・・以下省略(自動返信の送信)・・・
  
}

complete.php コードの全文を別ページで開く