2026年5月29日
2026年5月29日
PHPのメール送信(PHPMailer・SwiftMailer)実践編:DKIM署名・OAuth2認証・一括送信とローカルでのテスト
PHPMailer で SMTP 送信ができるようになったら、次に効いてくるのは「届くこと」と「安全に運用できること」です。この記事では、送信テストをローカルの受信サーバー Mailpit で行う方法、PHPMailer での DKIM 署名、パスワードの代わりに OAuth2 で SMTP 認証する方法、複数の宛先へ順に送るときの書き方、失敗したときのログの残し方を説明します。PHPMailer のインストールや基本の送信コード、SwiftMailer から Symfony Mailer への移行は別記事で扱っているので、ここではその先の運用に絞ります。
前提とバージョンの確認
PHPMailer は Composer で入れた 6.x 系を前提にします(2026年10月現在)。SwiftMailer は2021年に開発が終了しており、後継は Symfony Mailer です。新しく作るアプリで SwiftMailer を選ぶ理由はほぼありません。既存のコードに SwiftMailer が残っている場合も、この記事の DKIM や送信元ドメインの考え方はそのまま当てはまります。
composer require phpmailer/phpmailer
composer show phpmailer/phpmailer # 入っているバージョンを確認
SMTP のパスワードや OAuth2 のクライアントシークレットは、ソースコードに書かず、環境変数や Web 公開ディレクトリの外に置いた設定ファイルから読み込みます。以下のコードの getenv() はその前提です。設定ファイルを Git に含めないよう .gitignore にも追加してください。
送信テストはMailpitで行う
開発中に本物のアドレスへ送ると、誤送信や送信元の評価低下の原因になります。Mailpit は、受け取ったメールを外部へ送らずに保存し、ブラウザで中身を確認できる開発用の SMTP サーバーです。Docker で起動できます。
docker run -d --name mailpit -p 8025:8025 -p 1025:1025 axllent/mailpit
SMTP は localhost:1025、確認画面は http://localhost:8025 です。開発環境では PHPMailer の接続先をここに向けます。
$mail->isSMTP();
$mail->Host = getenv( 'SMTP_HOST' ) ?: 'localhost';
$mail->Port = (int) ( getenv( 'SMTP_PORT' ) ?: 1025 );
$mail->SMTPAuth = false; // Mailpit は認証なしで受け付ける
$mail->SMTPAutoTLS = false; // ローカルなので STARTTLS を試みない
本番では環境変数を差し替えるだけで本物の SMTP サーバーに切り替わるようにしておくと、テスト用の分岐をコードに残さずに済みます。Mailpit の画面ではヘッダーや HTML の表示、文字化けの有無を確認できるので、件名に日本語を入れたメールや添付ファイル付きのメールはここで一通り見ておきます。
DKIMで署名する
DKIM は、送信したドメインの持ち主がそのメールを送ったことを、電子署名で示す仕組みです。Gmail などは大量に送る送信者に SPF・DKIM・DMARC の設定を求めており、少量の送信でも DKIM があると迷惑メール扱いされにくくなります。
多くの場合、DKIM の署名は SMTP サーバーやメール配信サービスの側で行います。外部の配信サービスを使っているなら、まずそのサービスの管理画面で DKIM を有効にし、指定された DNS レコードを追加するのが近道です。自前の SMTP サーバーで署名していない場合に限り、PHPMailer 側で署名します。
鍵の作成は OpenSSL で行います。
openssl genrsa -out dkim_private.pem 2048
openssl rsa -in dkim_private.pem -pubout -out dkim_public.pem
公開鍵の中身(先頭と末尾の行を除いた部分)を、セレクター._domainkey.example.com という名前の TXT レコードとして DNS に登録します。値は v=DKIM1; k=rsa; p=公開鍵 の形式です。秘密鍵は Web から読めない場所に置き、PHP の実行ユーザーだけが読めるパーミッションにします。
$mail->DKIM_domain = 'example.com';
$mail->DKIM_selector = 'mail2026';
$mail->DKIM_private = '/etc/myapp/dkim_private.pem';
$mail->DKIM_passphrase = ''; // 鍵にパスフレーズを付けた場合に指定
$mail->DKIM_identity = $mail->From;
$mail->DKIM_copyHeaderFields = false;
DKIM_domain は差出人アドレス(From)のドメインと合わせます。ずれていると、DMARC の判定で「署名はあるがドメインが一致しない」扱いになります。送信後は、受信したメールのヘッダーにある Authentication-Results を開き、dkim=pass になっているかを確認します。Gmail なら「メッセージのソースを表示」に相当する画面で見られます。
パスワードの代わりにOAuth2で認証する
Google や Microsoft 365 のメールサーバーは、ユーザー名とパスワードだけの SMTP 認証を制限する方向に進んでいます。Google のアカウントでは2段階認証を有効にしたうえでのアプリパスワードが使える場合もありますが、組織の設定で無効にされていることもあります。Microsoft 365 は SMTP の基本認証を段階的に廃止する方針を発表しています。どちらも方針や時期が変わることがあるため、各社の開発者向けドキュメントで最新の状況を確認してください。
PHPMailer は XOAUTH2 という方式で OAuth2 のアクセストークンを使った認証に対応しています。トークンの取得には別途 OAuth2 のクライアントライブラリが必要です。Google の場合の例を示します。
composer require league/oauth2-google
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\OAuth;
use League\OAuth2\Client\Provider\Google;
$provider = new Google( array(
'clientId' => getenv( 'GOOGLE_CLIENT_ID' ),
'clientSecret' => getenv( 'GOOGLE_CLIENT_SECRET' ),
) );
$mail = new PHPMailer( true );
$mail->isSMTP();
$mail->Host = 'smtp.gmail.com';
$mail->Port = 587;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->SMTPAuth = true;
$mail->AuthType = 'XOAUTH2';
$mail->setOAuth( new OAuth( array(
'provider' => $provider,
'clientId' => getenv( 'GOOGLE_CLIENT_ID' ),
'clientSecret' => getenv( 'GOOGLE_CLIENT_SECRET' ),
'refreshToken' => getenv( 'GOOGLE_REFRESH_TOKEN' ),
'userName' => 'sender@example.com',
) ) );
リフレッシュトークンは、OAuth2 の同意画面で一度だけ取得し、以後はそれを使ってアクセストークンを自動で更新します。PHPMailer のリポジトリには、リフレッシュトークンを取得するための補助スクリプトが同梱されています。リフレッシュトークンはパスワードと同じ重さの秘密情報なので、ログに出したり、リポジトリに含めたりしないでください。Microsoft 365 の場合は、Microsoft 用のプロバイダーライブラリと、Entra ID でのアプリ登録が必要です。
複数の宛先へ順に送る
会員への通知など、同じ内容を複数の人に送るときに、宛先を To や Cc に並べると、受信者全員にほかの人のアドレスが見えてしまいます。1通ずつ宛先を変えて送るのが基本です。毎回 SMTP に接続し直すと遅いため、接続を保ったまま送ります。
$mail->SMTPKeepAlive = true; // 送信ごとに切断しない
foreach ( $recipients as $r ) {
try {
$mail->addAddress( $r['email'], $r['name'] );
$mail->Subject = 'お知らせ';
$mail->Body = 'こんにちは、' . $r['name'] . ' さん。';
$mail->send();
} catch ( \PHPMailer\PHPMailer\Exception $e ) {
error_log( 'mail failed: ' . $r['email'] . ' ' . $mail->ErrorInfo );
$mail->getSMTPInstance()->reset(); // エラー後の接続状態を戻す
}
$mail->clearAddresses();
$mail->clearAttachments();
usleep( 200000 ); // 0.2秒待つ(送信先サーバーへの負担を抑える)
}
$mail->smtpClose();
clearAddresses() を忘れると、前の宛先が残ったまま次のメールに追加され、宛先が雪だるま式に増えます。もっとも多い事故なので、ループの最後に置きます。
SMTP サーバーやメールサービスには、1時間・1日あたりの送信数の上限があります。上限を超えるとエラーになるか、アカウントが一時停止されることがあります。上限の値はサービスと契約によって違うため、利用中のサービスの資料で確認し、数百通を超える配信は、配信サービスの API やキューを使った仕組みに切り替えることを検討してください。
失敗を記録して原因を追う
PHPMailer を new PHPMailer( true ) で作ると、失敗時に例外が発生します。例外を捕まえて ErrorInfo をログに残しておくと、原因の切り分けが早くなります。接続段階の問題を調べたいときだけ、SMTP のやり取りを出力します。
use PHPMailer\PHPMailer\SMTP;
$mail->SMTPDebug = SMTP::DEBUG_SERVER;
$mail->Debugoutput = function ( $str, $level ) {
error_log( 'SMTP: ' . trim( $str ) );
};
デバッグ出力には認証のやり取りが含まれることがあるため、調査が終わったら SMTPDebug を SMTP::DEBUG_OFF に戻し、ログも削除します。
ログでよく見るエラーと、確認する順番は次のとおりです。
- 接続できない(タイムアウト): サーバーのファイアウォールやホスティング会社が、外向きの 587 番・465 番ポートを止めていることがあります。
openssl s_client -starttls smtp -connect smtp.example.com:587で、PHP を介さずに接続できるかを試します。 - 認証エラー(535 など): ユーザー名・パスワードの誤り、アプリパスワードが必要な設定、OAuth2 のトークンの期限切れや権限(スコープ)不足を順に確認します。
- 送信はできるのに届かない: 迷惑メールフォルダを見たうえで、受信側のヘッダーで SPF・DKIM・DMARC の結果を確認します。From のドメインと、実際に送っているサーバーや署名ドメインが合っているかが要点です。
入力値の扱い
問い合わせフォームのように、利用者の入力をメールに使う場合は、送信先のアドレスを利用者に選ばせないことが大前提です。差出人の欄には自分のドメインのアドレスを固定し、利用者のアドレスは addReplyTo() に入れます。入力されたアドレスは PHPMailer::validateAddress() で形式を確認し、件名や名前に改行が含まれていたら受け付けないようにします。HTML メールの本文に入力値を入れるときは、htmlspecialchars() でエスケープします。
WordPress の場合は、本体に PHPMailer が同梱されているので、自分で Composer から入れる必要はありません。wp_mail() を使い、SMTP の設定は phpmailer_init アクションで渡します。DKIM の項目も同じアクションの中で $phpmailer に設定できます。
まとめ
- 開発中は Mailpit などのテスト用 SMTP に送り、本番は環境変数で接続先を切り替える。
- DKIM は配信サービスやサーバー側で署名するのが基本で、自前で行う場合は From のドメインと署名ドメインをそろえ、
dkim=passを確認する。 - Google や Microsoft 365 はパスワード認証を制限する方向にあり、PHPMailer の
XOAUTH2で OAuth2 認証に移行できる。 - 複数の宛先へは
SMTPKeepAliveで接続を保ち、1通ごとにclearAddresses()で宛先を消す。 - 例外と
ErrorInfoをログに残し、接続・認証・到達の順に原因を切り分ける。