Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【PHP】PHPでSMTP認証を通して極上なメール送信をしてみよう【PHPMailer】

突然現れる技術系ブログっぽい記事...!

 以前に調べたことがあるので、PHP言語でのメール送信処理を部品化したコード例と一緒に上げていく記事です。主に初心者さんを対象にしています。動作確認したのが古めのPHP7.2ですが、そう変わるものでもないので他のバージョンでも同じかと思います。

PHPロゴのフォントはHandel Gothic、似ているHandel Gothic BTで入れました(ちょっとイマイチかも)

単純にメール送信する:mb_send_mail()関数があるよ

 単に送信したいだけなら、PHPの組み込み関数にそのものずばりの mail() 関数、日本語のマルチバイト文字を使うならこちらでしょう、ラッパー関数の mb_send_mail() が用意されています。パラメータに宛先メールアドレスやタイトルや本文を指定してコールするだけです。
 文字化けの話はよく目にするので、UTF-8で確実に処理しましょう。

www.php.net

 さてメール送信にはメール送信用のサーバーが必要です。上のPHPドキュメントには書かれておらず、ネット上の簡単な記事にも載っていないことが多いのですが、このメールサーバーの設定はPHPをインストールした時にインストールした先のディレクトリにある基本の設定ファイル、 php.ini に設定されています。
 PHPのバージョンによってファイル内の場所が微妙に違うのですが、以下のようになっています。

[mail function]
; For Win32 only.
; http://php.net/smtp
SMTP = localhost
; http://php.net/smtp-port
smtp_port = 25

; For Win32 only.
; http://php.net/sendmail-from
;sendmail_from = me@example.com

; For Unix only.  You may supply arguments as well (default: "sendmail -t -i").
; http://php.net/sendmail-path
;sendmail_path =

未設定のまま関数を呼ぶと以下のようなエラーになります。

PHP Warning:  mb_send_mail(): "sendmail_from" not set in php.ini or custom "From:" header missing in XXX.php on line xxx

php.iniに書かれた内容はPHPファイルの実行時にプログラム側から上書きすることもできますが、言語の実行環境自体の設定ファイルに事前に定義が必要なのはちょい使い勝手が悪いですね。

 そしてメール用サーバーのホスト名(かIP)とポート番号さえ分かればそこを利用できるのであれば、誰もがそこからメールを送信できてしまいます。
 ふつうはユーザーとパスワードで認証し、正しい時だけメール送信ができるような認証の仕組み、「SMTP認証」もしくは「SMTP-AUTH」と呼ばれる仕組みが備えられています。この仕組みを持ったメール送信サーバーはそのまま「SMTPサーバー」と呼ばれます。
 SMTPは略語だけ頻出するので何の略か時々忘れがちですが、SMTP: Simple Mail Transfer Protocol の略ですね。情報処理試験にもよく出てくるやつです。このプロトコルに従ってSMTPクライアント←→SMTPサーバー間で通信してメール送信のリクエストを渡す部分を、各プログラミング言語ごとのライブラリがやってくれる形になります。

ja.wikipedia.org

 話を戻してPHPの組み込み関数の mail()mb_send_mail()はそのままではSMTPサーバーに対応していません。試しにやると以下のように、認証で弾かれた旨がエラーで返ってきます。

PHP Warning:  mb_send_mail(): SMTP server response: xxx x.x.x <unknown[xx.xx.xxx.xx]>: Client host rejected: Access denied in XXX.php on line xxx

SMTP認証を通してメール送信する:PHPMailerというライブラリがあるよ

 ということでSMTP認証に対応したメール送信の手段は...と探していくと、PHPでは PHPMailer というライブラリが主流のようです。英語版Wikipediaに項目がある ぐらいの老舗のライブラリですね。

github.com

 この記事ではバージョン6.6.5で確認しています。なおバージョン6.4.1より前のものには脆弱性があることが2021年5月に報告され、すぐに対応されています。最新バージョンを使いましょう。

www.security-next.com

ライブラリ管理ツール Composer を使って PHPMailer をインストールする

 標準的な以下のコマンド実行で、この例では作成した phpmailer ディレクトリに一式がダウンロードできます。

$ mkdir phpmailer
$ cd phpmailer
$ composer require phpmailer/phpmailer
手動で PHPMailer をインストールする

 ネットワークの問題があってComposerが使えなかったり、既存のPHPアプリケーション側の都合などでで手動で入れたい場合もあると思います。その際は以下です。

  1. 上記 PHPMailerのページで「<Code>」ボタン-[Download ZIP]
  2. ファイル名は恐らく PHPMailer-master.zip なので任意の名前にリネームして解凍。ここでは PHPMailer
  3. 解凍した PHPMailer/src/*.php がライブラリ本体。メインはこの中の PHPMailer.php

メール送信部品として組み込んでみよう

 規模の小さなコードであれば上記ライブラリをそのまま呼んでもいいのですが、ある程度の規模のアプリケーションになると部品化して使い勝手をよくし、呼び出し側から見た場合に依存性を下げて凝縮度を上げる工夫が必要になってきます。

  • SMTP認証のユーザーとパスワードなど機密性の高い値はコード直書き絶対禁止、おそらく設定ファイルやDBや他のクラスに保持しているので、外部から設定できる方が望ましい。
  • SMTPサーバーを複数使うケースもありそう。でもメールを送る毎回毎回別というほどでもなさそうなので、クラスの中に保持しよう。
  • メール送信処理実行のたびに宛先や内容は変わると思うので、こちらはクラスのメソッドコールのたびに指定できるようにしよう。

と軽く設計を考えて、このサンプルでは以下のようにしています。

\PHP-SMTP-MAIL-SENDER-SAMPLE
│  client_sample.php # 呼び出し元のサンプル
│  SMTPMailSenderSample.php  # メール送信部品のサンプル
│
└─libs
    └─PHPMailer # この下がダウンロードしたライブラリ一式
        ├─language
        ├─src # この下がライブラリのクラス本体

よく考えるとライブラリが1つしか置いてないのにディレクトリ名 libs とはこれ如何に...とセルフツッコミが入ったのですが気にしないことにします。 以下、すべてファイルのエンコードUTF-8です。

メール送信部品のサンプルクラス SMTPMailSenderSample.php
  • ライブラリPHPMailerのうち実際に必要なクラスは3つだけなので、先頭で読み込んでいます。
  • 中身は簡単で、クラスのプロパティ(=メンバ変数) $mailerPHPMailer側のクラスインスタンスを保持しています。
  • コンストラクタは書いてある通りでインスタンス$mailerを生成しているだけです。
    • 文字エンコードはもうUTF-8必須なので決め打ちしています。
    • この例では暗号化を有効化していないので必要なら変えてください。
    • ポート番号はSMTPサーバーでは25が普通なので引数のデフォルト値を入れています。
  • メール送信を行うsendメソッドもコメントに各種書いてある通りです。
    • 送信者は配列でメールアドレスのみ、メールアドレスと送信者名とどちらでも動くようにしています。後者にすると、メールソフトで受け取った際に送信者名も併せて表示されます。
    • メソッド自体は送信が成功した時だけtrueを返し、入力の引数がどれか不正だったらfalseを早期に返し、送信中にエラーが発生したら例外スローで終了です。
    • このクラスは部品なので、エラーのロギングなどは行わず、呼び出し側に結果を返すだけになっています。
<?php

namespace util;


// アプリケーション全体の初期処理などで読み込むと常に読み込まれてしまうので、この部品クラスをコールした必要な時だけにする
require_once __DIR__ . './libs/PHPMailer/src/PHPMailer.php';
require_once __DIR__ . './libs/PHPMailer/src/SMTP.php';
require_once __DIR__ . './libs/PHPMailer/src/Exception.php';

/**
 * SMTPを用いたメール送信部品のサンプルクラス。
 * @package util
 * @author iwasiman
 */
class SMTPMailSenderSample
{
    private $mailer = null; //  PHPMailer/PHPMailer ライブラリのクラス

    /**
     * コンストラクタ。
     *
     */
    public function __construct(string $host, ?int $port = 25, string $userName, string $password)
    {
        // インスタンスを生成(true指定で例外を有効化する)
        $mailer = new \PHPMailer\PHPMailer\PHPMailer(true);
        // 文字エンコードを指定
        $mailer->CharSet = 'utf-8';
        // SMTPサーバの設定
        $mailer->isSMTP(); // SMTPの使用宣言
        $mailer->Host = $host; // SMTPサーバーを指定
        $mailer->SMTPAuth = true; // SMTP authenticationを有効化
        $mailer->Username = $userName; // SMTPサーバーのユーザ名
        $mailer->Password = $password; // SMTPサーバーのパスワード
        $mailer->SMTPSecure = false; // 暗号化を有効するなら'tls' or 'ssl' / 無効の場合はfalse
        $mailer->Port = $port; // TCPポートを指定(tlsの場合は465や587)
        $this->mailer = $mailer;
    }

    /**
     * メールを送信します。
     *
     * @param string $email 宛先のメールアドレス(空文字不可)
     * @param array $fromHeader Fromヘッダーを表す文字列配列(長さは1で送信者メールアドレスか、長さ2で送信者メールアドレスと送信者名を指定)
     * @param string $subject メールのタイトル (空文字OK)
     * @param string $body メールの本文(空文字不可)
     * @return boolean true: メール送信に成功 / false: 引数のどれかが間違っている
     * @throws Exception 例外 メール送信時にエラーが発生した場合。
     */
    public function send(string $email, array $fromHeader, string $subject, string $body): bool
    {
        $result = false;
        // Fromヘッダーが正しいかチェックして送信者に設定
        if (count($fromHeader) == 0 || count($fromHeader) > 2) {
            return $result;
        }
        $fromEmail = $fromHeader[0];
        if (is_null($fromEmail) || strlen($fromEmail) === 0) {
            return $result;
        }
        $fromName = null;
        if (count($fromHeader) == 2) {
            $fromName = $fromHeader[1];
        }
        // 送信者 引数$fromHeaderの2つめの送信者名は、nullでなく空文字でもない場合のみメールに入る
        if (is_null($fromName)) {
            $this->mailer->setFrom($fromEmail);
        } else {
            $this->mailer->setFrom($fromEmail, $fromName);
        }

        // 宛先メールアドレスをチェックして設定
        if (strlen($email) === 0) {
            return $result;
        }
        $this->mailer->addAddress($email);

        // メールのタイトル設定(空でも送信されます)
        $this->mailer->Subject = $subject;
        // メールの本分のチェックと設定
        // メール本文が空だとPHPMailerが"Message body empty"のエラーを出すので、送信前に判定
        if (strlen($body) === 0) {
            return $result;
        }
        $this->mailer->Body = $body;

        // メインの送信処理
        try {
            $result = $this->mailer->send();
        } catch (\Exception $e) {
            throw $e;
        }
        return $result;
    }
}

 ライブラリPHPMailerの呼び出し部分をこのクラスで包み込むことでクラス外部からは内部を隠蔽し、クラス外部から呼ぶ際には一切気にしなくてよくなりました。「ラップする」とか「ラッパーオブジェクト」と呼ばれる設計手法でよく使われるものです。  こうすることで、将来以下のような事態が起こっても変更は基本的に SMTPMailSenderSample クラス内に閉じます。サンプル程度で大げさですが、コード修正量とテストの範囲が減り、保守性、変更容易性、拡張性を高めることができます。

  • PHPMailerのバージョンが変わって呼び出し方が変わった
  • PHPMailerがメンテされなくなったり開発が中止されたり脆弱性が見つかったりして、別のライブラリを使うことになった
  • 呼び出し側のアプリケーションや連携した別アプリのメール送信の仕組みを使うことになった
  • インターネット上の別のSaaSとか、AWSのSESとか、別の何かを呼び出して処理することになった
呼び出し元のサンプル client_sample.php
  • 何のことはない、上記の SMTPMailSenderSample クラスのインスタンスを作って sendメソッドを呼んでいるだけです。
  • サンプルは関数にすらしていませんが、実際の使用時では、恐らくビジネスロジック層的なレイヤーに属するクラスの関数の中にこの処理が入るような感じでしょうか。
  • SMTPMailSenderSample クラスのインスタンス生成時
    • SMTP認証のユーザーとパスワードを直書きしていますが、実際は外部からアクセスできない場所から取得するようご注意ください。
    • このSMTPサーバーが2つ以上あるなら、インスタンス生成を別メソッドで行うとか、生成専用のファクトリクラスやファクトリメソッド経由で作るなどでしょうか。
  • メール組み立て部分は実装の通りで、必要に応じ内部メソッドで組み立てたりする感じでしょうか。
  • メール送信の実処理も実装とコメントの通りです。
    • 入力に不備があったら変数$sendResultfalseのまま送信せずに抜けるのでそこで処理できます。
    • 送信の試み自体は行われてかつ失敗すると、catch節に落ちます。コメントに書いた通り$eが持っている各種メッセージで原因を判別できます。
    • ちなみにこのエラーメッセージは英語ですが、PHPMailer/language/phpmailer.lang-ja.phprequireで読み込んでおくと日本語にもなるそうです。
    • catch節に落ちずに送信が正常終了すると変数$sendResulttrueで返るので、そこで処理できます。
    • 送信が正常終了せず、かつ例外は発生しなかった場合はelse節で一応判定できますが、検証では異常時は全て例外スローでした。たぶんここに来るケースはないと思います。
<?php
declare(strict_types = 1);
require_once __DIR__ . './SMTPMailSenderSample.php';

use util\SMTPMailSenderSample;

// メール送信部品の呼び出し側コードのサンプルです。

// 必要な値は設定ファイル、DB、他の処理などから取得し、部品クラスを生成します。
$host = "sample.mail.com";
$port = 25;
$user = "sample-smtp-user";
$pass = "sample-smtp-pass";
$mailSender = new SMTPMailSenderSample($host, $port, $user, $pass);

// 必要な値は設定ファイル、DB、他の処理などから取得し、メール1件の情報を組み立てます。
// 宛先メールアドレス
$email = "anonymous@republic.gov";
// ヘッダーにはFROMアドレスと送信者を指定 以下どちらでも動きます
$fromHeader = ['mirage-palace@republic.gov'];
$fromHeader = ['mirage-palace@republic.gov', '陽炎パレス'];
// 通知メールのタイトルを組み立て
$mailSubject = "(作画とキャラデザが)";
// 通知メールの本文を組み立て
$mailBody = "極上だ...";

// メール送信を行います。
$sendResult = false;
try {
    $sendResult = $mailSender->send($email, $fromHeader, $mailSubject, $mailBody);
    if (!$sendResult) {
        // TODO: ここに来れば呼び出し時の引数がおかしい場合です。呼び出し側の処理から早期リターンで抜けるなど。
    }
} catch (\Exception $e) {
    // TODO: エラー処理。ログ記録などを行ってください。
    //var_dump($e->getMessage());
    // ホストが違う,ポート番号が不正: "SMTP Error: Could not connect to SMTP host. Failed to connect to server"
    // 認証に使うユーザー、パスワードが不正:"SMTP Error: Could not authenticate."
    // Fromアドレスのメアドが正しくない: "Invalid address:  (From): (ここにメアドの値)"
    // Fromアドレスのメアドが、認証に使うユーザーの保有するものではない: "Sender address rejected: not owned by user (ここにユーザー名)"
    // メール本文が空: "Message body empty"が出る前に部品側でfalseで終了するようにしています
}
if ($sendResult) {
    // TODO: ここまで来ればメール送信が成功しています。成功時のみの処理など。
} else {
    // 送信失敗時は例外に落ちるはずですが、例外が発生せず送信失敗した場合はこちらに来ます。
}

 ということで入力値チェックやエラー処理も丁寧に行うと若干長くなりましたが、メール送信のサンプルでした。なおこのサンプルは以下の場合には未対応なので、必要なら送信部品クラス側を拡張する形になります。

  • 一度に複数のメールアドレスに同時に送る
  • CcやBcc、Reply-To、添付ファイルなどを指定する(PHPMailer側は対応しています)

メール送信できました

まとめ:分かれば意外と簡単にメール送信できたよ

 ということで無事に、陽炎パレスから極上なメールが出せるようになりました。でもよく考えると電子メールより、暗号文を鳥の脚に括り付けて送ったりしたほうがスパイっぽいよね! (どうしてもコードサンプルにネタを入れたくなるスタイル...)
 現実世界ではSMTPが標準になったのは1982年だそうなので、東西冷戦時代よりもっと後ですね。

完全なサンプル一式はGitHubの以下にあります。

github.com

最近のPHP言語入門書というと以下でしょうか。