「PHPMailer」使用手順、セキュリティ(機密情報設定ファイルは別配置)、Docker開発から本番環境へアップロード

ローカルDocker環境でPHPMailerを使用する初心者向けの手順

1)プロジェクトディレクトリ

project-dir
  ├dockerfile
  └docker-compose.yml

2)Dockerfile作成

Dockerfileについて

Dockerfileに記述された指示に従って、Dockerイメージ(設計図)を作成します

▽Dockerfile

3)docker-compose.yml作成

version: '3'
services:
  web:
    build: .
    ports:
      - "8080:80"
    volumes:
      - ./src:/var/www/html

4)project-dirにて「docker-compose build「docker-compose up -d

  1. docker-compose build:
    • このコマンドは Dockerfile に基づいてイメージをビルドします。
  2. docker-compose up -d:
    • このコマンドはコンテナを起動し、バックグラウンドで実行します。
    • Docker Compose ファイルで定義されたボリュームをマウントします。
      ※もし指定されたホスト側のディレクトリ(この場合は src)が存在しない場合、Docker は自動的にそれを作成します。

「docker desktop」でContainer作成が確認できます

5)index.php send_mail.phpを作成

project_root/
├── Dockerfile
├── docker-compose.yml
└── src/
     ├── index.php
     └── send_mail.php

▽index.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PHPMailerテスト</title>
</head>
<body>
    <h1>PHPMailerテスト</h1>
    <form action="send_mail.php" method="post">
        <label for="to">宛先:</label>
        <input type="email" id="to" name="to" required><br><br>
        
        <label for="subject">件名:</label>
        <input type="text" id="subject" name="subject" required><br><br>
        
        <label for="message">本文:</label><br>
        <textarea id="message" name="message" rows="4" cols="50" required></textarea><br><br>
        
        <input type="submit" value="送信">
    </form>
</body>
</html>

▽send_mail.php

<?php

use PHPMailer\PHPMailer\PHPMailer;// PHPMailerライブラリの読み込み
use PHPMailer\PHPMailer\Exception;// PHPMailerライブラリの読み込み

require 'vendor/autoload.php';// PHPMailerライブラリの読み込み

if ($_SERVER["REQUEST_METHOD"] == "POST") {
    $mail = new PHPMailer(true); //PHPMailerのインスタンス作成(trueは例外を有効にする)

    try {
        //SMTPサーバー:Gmailの設定
        $mail->isSMTP();
        $mail->Host       = 'smtp.gmail.com';  // SMTPサーバーを指定
        $mail->SMTPAuth   = true;
        $mail->Username   = 'yourmail@gmail.com';  // SMTPユーザー
        $mail->Password   = 'your app pass';          // SMTPパスワード
        $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
        $mail->Port       = 465;

        //送信元、送信先の設定
        $mail->setFrom('yourmail@gmail.com', 'Mailer');
        $mail->addAddress($_POST['to']);

        //メール本文
        $mail->isHTML(true);
        $mail->Subject = $_POST['subject'];
        $mail->Body    = $_POST['message'];

        $mail->send();
        echo 'メッセージが送信されました';

    } catch (Exception $e) {
        echo "メッセージを送信できませんでした。Mailer Error: {$mail->ErrorInfo}";
    }
}

6)コンテナ内でPHPMailerをインストールします:

docker-compose exec web composer require phpmailer/phpmailer

7)Googleアプリパスワード生成

Google→セキュリティ→2段階認証プロセス→アプリパスワードから設定

send_mail.phpを編集

注意)本番環境では、これらの設定を環境変数や別の設定ファイルに移動し、Gitなどのバージョン管理システムにコミットしないようにすることをおすすめします。

セキュリティ面

サニタイズについて

サニタイズ(sanitize)とは、ユーザーから入力されたデータを安全使用可能な形式に変換することを指します。

セキュリティヘッダ

セキュリティを強化するために使用される特別なHTTPヘッダ

  • header(“X-XSS-Protection: 1; mode=block”); //ブラウザの組み込みのXSS対策フィルターを有効にし、攻撃を検出したら、ページの読み込みをブロックします。
  • header(“X-Frame-Options: SAMEORIGIN”); //ページを<frame>、<iframe>、<embed>、<object>で表示することを許可します。ただし、同じオリジンの場合のみ。(クリックジャッキング対策)
  • header(“X-Content-Type-Options: nosniff”); //ブラウザがコンテンツタイプをスニッフィングしないようにします。
  • header(“Referrer-Policy: strict-origin-when-cross-origin”); //クロスオリジンのリクエストに対しては、Referer ヘッダーにはリクエスト元のオリジンのみを含めます。
  • header(“Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘unsafe-inline’ ‘unsafe-eval’; style-src ‘self’ ‘unsafe-inline’;”); //様々な攻撃(XSS、データ注入など)からサイトを保護します。

サニタイズ、セキュリティヘッダを導入のためPHPファイルを修正する

▽send_mail.php

<?php
// ob_start(); は、「出力を一時的に裏側で溜めておく」という命令
// もし途中でエラーメッセージが表示されたり、予期せぬ出力があったりすると、セキュリティヘッダーを設定できなくなるため
ob_start();

// セッションが開始されていない場合のみ、設定を変更してセッションを開始
if (session_status() == PHP_SESSION_NONE) {    // セッション関連の設定
    ini_set('session.cookie_httponly', 1);
    ini_set('session.cookie_secure', 1);        // セッションを開始
    session_start();
} else {
    // セッションが既に開始されている場合は、そのまま続行
    session_start();
}

// CSRFトークンがセッションに存在しない場合は生成
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// セキュリティヘッダーの設定
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: SAMEORIGIN");
header("X-Content-Type-Options: nosniff");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';");

use PHPMailer\PHPMailer\PHPMailer;// PHPMailerライブラリの読み込み
use PHPMailer\PHPMailer\Exception;// PHPMailerライブラリの読み込み

require 'vendor/autoload.php';// PHPMailerライブラリの読み込み

if ($_SERVER["REQUEST_METHOD"] == "POST") {

    // CSRFトークンの検証
    if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        // エラーをログに記録
        error_log("CSRF token mismatch. POST token: " . ($_POST['csrf_token'] ?? 'not set') . ", Session token: " . ($_SESSION['csrf_token'] ?? 'not set'));
        die('セッションが期限切れか無効です。ページを更新して再度お試しください。');
    }

    // 入力のサニタイズとバリデーション
    $to = filter_var($_POST['to'], FILTER_SANITIZE_EMAIL);
    $subject = htmlspecialchars($_POST['subject'], ENT_QUOTES, 'UTF-8');
    $message = htmlspecialchars($_POST['message'], ENT_QUOTES, 'UTF-8');

    // メールアドレスの妥当性チェック
    if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
        die('無効なメールアドレスです。');
    }

    $mail = new PHPMailer(true); //PHPMailerのインスタンス作成(trueは例外を有効にする)

    try {
        //SMTPサーバー:Gmailの設定
        $mail->isSMTP();
        $mail->Host       = 'smtp.gmail.com';  // SMTPサーバーを指定
        $mail->SMTPAuth   = true;
        $mail->Username   = 'yourmail@gmail.com';  // SMTPユーザー
        $mail->Password   = 'your app pass';          // SMTPパスワード
        $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
        $mail->Port       = 465;

        //送信元、送信先の設定
        $mail->setFrom('yourmail@gmail.com', 'Mailer');
        $mail->addAddress($to);

        //メール本文
        $mail->Subject = $subject;
        $mail->Body    = $message;
        $mail->AltBody = strip_tags($message); //HTMLタグを除去

        $mail->send();
        echo 'メッセージが送信されました';

    } catch (Exception $e) {
        // 詳細なエラー情報を隠し、ログに記録します。
        error_log("メール送信エラー: " . $mail->ErrorInfo);
        echo "メッセージを送信できませんでした。管理者にお問い合わせください。";
    }

    // 新しいCSRFトークンの生成
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));

} else {
    // POSTメソッド以外でアクセスされた場合の処理
    die('不正なアクセスです。');
}

// 出力バッファリングを終了し、出力を送信
ob_end_flush();

▽index.php

<?php
session_start();
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
} // CSRFトークンがセッションに存在しない場合は生成
?>

機密情報を含む設定ファイルを作成

メリット

  • セキュリティ向上:機密情報(パスワードなど)をGitリポジトリにコミットせずに済みます。
    (.gitignoreに追加すると)
  • ポータビリティの向上:異なるサーバーや環境への移行が容易

1)プロジェクトのルートディレクトリに configフォルダその中にconfig.php を作成します

project-dir\config\config.php

<?php
define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.example.com');
define('SMTP_USER', getenv('SMTP_USER') ?: 'user@example.com');
define('SMTP_PASS', getenv('SMTP_PASS') ?: 'password');

2)上記修正に伴いsend_mail.php、docker-compose.ymlも修正

▽send_mail.php

…

require 'vendor/autoload.php';// PHPMailerライブラリの読み込み
require_once __DIR__ . '/../config/config.php';
…


        //SMTPサーバー:Gmailの設定
        $mail->isSMTP();
        $mail->Host       = SMTP_HOST; // SMTPサーバーを指定
        $mail->Username   = SMTP_USER; // SMTPユーザー
        $mail->Password   = SMTP_PASS; // SMTPパスワード
        $mail->SMTPAuth   = true;
        $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
        $mail->Port       = 465;

▽docker-compose.yml

    volumes:
      - ./src:/var/www/html
      - ./config:/var/www/config.php

3)2)の修正を行った後、Dockerコンテナを再ビルドして起動

docker-compose down
docker-compose build
docker-compose up -d

2)プロジェクトのルートディレクトリに .gitignore ファイルを作成

▽.gitignore

/config/config.php

本番環境のさくらインターネットへアップロード

ローカルDocker開発環境、さくらインターネットの本番環境の両環境併用できるよう下記の通り修正

▽config.php

<?php
// 環境の判別
$is_local = ($_SERVER['SERVER_NAME'] == 'localhost' || $_SERVER['SERVER_ADDR'] == '127.0.0.1');

// デバッグモードの設定
define('DEBUG_MODE', $is_local); // ローカルではデバッグモードON、本番では OFF
// 本番環境で一時的にデバッグモードを有効化する場合
// define('DEBUG_MODE', true);  // コメントを外して使用


if ($is_local) {
    // ローカル環境(Docker)の設定
    define('SMTP_HOST', getenv('SMTP_HOST') ?: 'smtp.gmail.com');
    define('SMTP_USER', getenv('SMTP_USER') ?: 'xxxx@gmail.com');
    define('SMTP_PASS', getenv('SMTP_PASS') ?: 'xxxx');
    define('SMTP_PORT', 587);
    define('SMTP_SECURE', 'tls');
} else {
    // さくらインターネット環境の設定
    define('SMTP_HOST', 'xxxx');
    define('SMTP_USER', 'xxxx');
    define('SMTP_PASS', 'xxxx');
    define('SMTP_PORT', 587);
    define('SMTP_SECURE', 'tls');
}

// サイトの URL
define('SITE_URL', $is_local ? 'http://localhost:8080' : 'xxxx');

さくらインターネットのメール情報の確認方法の参考サイト

下記のホスト、ユーザ名、パスワードについて

  • define(‘SMTP_HOST’, ‘xxxx’);
  • define(‘SMTP_USER’, ‘xxxx’);
  • define(‘SMTP_PASS’, ‘xxxx’);

さくらのメールボックスを PHPMailer で SMTP + STARTTLS で送信する時の注意点https://qiita.com/ameyamashiro/items/c7283bd1ec5dd3146ef9

▽send_mail.php

<?php
// ob_start(); は、「出力を一時的に裏側で溜めておく」という命令
// もし途中でエラーメッセージが表示されたり、予期せぬ出力があったりすると、セキュリティヘッダーを設定できなくなるため
ob_start();

// 設定ファイルの読み込み
if (strpos(__DIR__, '/home/siennahare23') !== false) {
    // さくらインターネット環境
    require_once '/home/siennahare23/config/config.php';
} else {
    // ローカル環境
    require_once __DIR__ . '/../config/config.php';
}

// セッションが開始されていない場合のみ、設定を変更してセッションを開始
if (session_status() == PHP_SESSION_NONE) {
    // セッション関連の設定
    ini_set('session.cookie_httponly', 1);
    ini_set('session.cookie_secure', 1);
    
    // セッションを開始
    session_start();
} else {
    // セッションが既に開始されている場合は、そのまま続行
    session_start();
}

// CSRFトークンがセッションに存在しない場合は生成
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// セキュリティヘッダーの設定
header("X-XSS-Protection: 1; mode=block"); //ブラウザの組み込みのXSS対策フィルターを有効にし、攻撃を検出したら、ページの読み込みをブロックします。
header("X-Frame-Options: SAMEORIGIN"); //ページを<frame>、<iframe>、<embed>、<object>で表示することを許可します。ただし、同じオリジンの場合のみ。(クリックジャッキング対策)
header("X-Content-Type-Options: nosniff"); //ブラウザがコンテンツタイプをスニッフィングしないようにします。
header("Referrer-Policy: strict-origin-when-cross-origin"); //クロスオリジンのリクエストに対しては、Referer ヘッダーにはリクエスト元のオリジンのみを含めます。
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"); //様々な攻撃(XSS、データ注入など)からサイトを保護します。

use PHPMailer\PHPMailer\PHPMailer;// PHPMailerライブラリの読み込み
use PHPMailer\PHPMailer\Exception;// PHPMailerライブラリの読み込み

require 'vendor/autoload.php';// PHPMailerライブラリの読み込み

if ($_SERVER["REQUEST_METHOD"] == "POST") {

    // CSRFトークンの検証
    if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        // エラーをログに記録
        error_log("CSRF token mismatch. POST token: " . ($_POST['csrf_token'] ?? 'not set') . ", Session token: " . ($_SESSION['csrf_token'] ?? 'not set'));
        die('セッションが期限切れか無効です。ページを更新して再度お試しください。');
    }

    // 入力のサニタイズとバリデーション
    $to = filter_var($_POST['to'], FILTER_SANITIZE_EMAIL);
    $subject = htmlspecialchars($_POST['subject'], ENT_QUOTES, 'UTF-8');
    $message = htmlspecialchars($_POST['message'], ENT_QUOTES, 'UTF-8');

    // メールアドレスの妥当性チェック
    if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
        die('無効なメールアドレスです。');
    }

    $mail = new PHPMailer(true); //PHPMailerのインスタンス作成(trueは例外を有効にする)

    // デバッグモードの設定
    // $mail->SMTPDebug = 3; // デバッグ出力を有効化
    $mail->Debugoutput = 'html'; // デバッグ出力形式をHTMLに設定

    try {
        //SMTPサーバー:Gmailの設定
        $mail->isSMTP();
        $mail->Host       = SMTP_HOST; // SMTPサーバーを指定
        $mail->Username   = SMTP_USER; // SMTPユーザー
        $mail->Password   = SMTP_PASS; // SMTPパスワード
        $mail->SMTPAuth   = true;
        $mail->SMTPSecure = SMTP_SECURE;
        $mail->Port       = SMTP_PORT;

        //送信元、送信先の設定
        $mail->setFrom(SMTP_USER, 'Mailer');
        $mail->addAddress($to);

        //メール本文
        $mail->isHTML(true);
        $mail->Subject = $subject;
        $mail->Body    = $message;
        $mail->AltBody = strip_tags($message); //HTMLタグを除去

        $mail->send();
        echo 'メッセージが送信されました';

    } catch (Exception $e) {

        if (DEBUG_MODE) {
            echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}<br>";
            echo "Detailed error: " . $e->getMessage();
        } else {
            echo "メッセージを送信できませんでした。管理者にお問い合わせください。";
        }
        error_log("メール送信エラー: " . $mail->ErrorInfo);
    }

    // 新しいCSRFトークンの生成
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));

} else {
    // POSTメソッド以外でアクセスされた場合の処理
    die('不正なアクセスです。');
}

// 出力バッファリングを終了し、出力を送信
ob_end_flush();

アップロード

<ローカルのディレクトリ構成>

project_root/
│
├── src/
│   ├── index.php
│   ├── send_mail.php
│   └── vendor/ ...
├── config/
└── config.php
│
└── docker-compose.yml

<本番環境のディレクトリ構成>

/home/xxxx/www/xxxx/
│
├── contact/
│  │
│   ├── index.php
│   ├── send_mail.php
│   └── vendor/ ...
│
└── config/
    └── config.php

実際のフォーム