【CakePHP】 続・CakeEmail クラスに RFC 非準拠のメールアドレスを許容させる ~メールアドレスの正規化を試みる~

【CakePHP】 続・CakeEmail クラスに RFC 非準拠のメールアドレスを許容させる ~メールアドレスの正規化を試みる~

平成ライダー対昭和ライダー 仮面ライダー大戦 feat.スーパー戦隊』を劇場でみてきました。公開前にはいろいろ言っていましたが、ふたを開けてみたら予定外に満足している kagata ですこんにちは。「早く死んでくれないかな?」って、草加雅人に言われてみたい。

さて、本日は前回記事の続きをやります。

前回記事では、CakeEmail クラス内部で実施されるメールアドレスのバリデーションを制御するために、CakeEmail::emailPattern() でメールアドレスの正規表現を独自に設定するという手法を紹介しました。

今回はもう少し掘り下げて、モデルのバリデーションのようにバリデーションルールを単体の正規表現でなく独自のメソッドとして組み込んでみます。そうすることで、正規表現だけでは難しい要件にも柔軟に対応できるかもしれません。

外部からの反響

今回ご紹介する方法を考えるきっかけになったのは、さる身近な方面からちょうだいした次のようなリクエストでした。

任意の文字列をメールアドレスとして受け入れるというのは、どうにも心もとない。CakePHP 標準のメールアドレスバリデーションはある程度生かして、携帯電話の RFC 非準拠なメールアドレスだけうまく通過させる方法はないか?例えば次のような↓
CakePHP で docomo と au の RFC 非準拠メールアドレスを例外的に許可する email バリデーション - 頭ん中

ドメインで携帯電話のメールアドレスかどうか判断して、携帯電話のものだった場合は RFC 非準拠の箇所を直してから標準のバリデーションメソッドに投げるという寸法ですね。なるほど、これは巧妙…!

しかし、CakeEmail::emailPattern() で与えられる1件の正規表現だけでこれを実現するのは相当に骨が折れそうです。かといって、CakeEmail クラスにはバリデーションルールをメソッドの形で外部から渡す方法が用意されていません。

ということで、バリデーションのための独自のメソッドを CakeEmail のサブクラスに組み込んでみることにしました。

バリデーションメソッドを独自に定義したクラス

できたのがこちら。

App::uses('CakeEmail', 'Network/Email');

class CakeEmailNormalized extends CakeEmail {
    /**
     * Set email
     * (ほぼCakeEmail.phpからコピペ)
     */
    protected function _setEmail($varName, $email, $name) {
        if (!is_array($email)) {
            if (!$this->validateEmail($email)) { // 変更点1
                throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $email));
            }
            if ($name === null) {
                $name = $email;
            }
            $this->{$varName} = array($email => $name);
            return $this;
        }
        $list = array();
        foreach ($email as $key => $value) {
            if (is_int($key)) {
                $key = $value;
            }
            if (!$this->validateEmail($email)) { // 変更点2
                throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $key));
            }
            $list[$key] = $value;
        }
        $this->{$varName} = $list;
        return $this;
    }

    /**
     * Add email
     * (ほぼCakeEmail.phpからコピペ)
     */
    protected function _addEmail($varName, $email, $name) {
        if (!is_array($email)) {
            if (!$this->validateEmail($email)) { // 変更点3
                throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $email));
            }
            if ($name === null) {
                $name = $email;
            }
            $this->{$varName}[$email] = $name;
            return $this;
        }
        $list = array();
        foreach ($email as $key => $value) {
            if (is_int($key)) {
                $key = $value;
            }
            if (!$this->validateEmail($email)) { // 変更点4
                throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $key));
            }
            $list[$key] = $value;
        }
        $this->{$varName} = array_merge($this->{$varName}, $list);
        return $this;
    }

    /**
     * メールアドレスのバリデーションを実施する
     * @param $email メールアドレス文字列
     * @return bool バリデーションを通過したか否か
     */
    protected function validateEmail($email) {
        // docomo.ne.jp' もしくは 'ezweb.ne.jp' ドメインのメールアドレスは正規化する
        $normalized = preg_replace_callback(
            '/.+@(docomo|ezweb)\.ne\.jp$/i',
            array($this, 'normalizeEmail'),
            $email
        );

        // メールアドレスをCakePHP標準のバリデーションにかける
        return Validation::email($normalized, false, $this->_emailPattern);
    }

    /**
     * かつてdocomoやEZwebで取得できたRFCに準拠しないメールアドレスを正規化するコールバックメソッド
     * @param $matches 'docomo.ne.jp' もしくは 'ezweb.ne.jp' ドメインのメールアドレス文字列
     * @return mixed 正規化したメールアドレス文字列
     */
    protected function normalizeEmail($matches) {
        // 「ドットが連続する」もしくは「ローカルパートがドットで終わる」場合はその箇所を置換
        return preg_replace(array('/\.{2,}/', '/\.@/'), array('.', '@'), $matches[0]);
    }
}

ずいぶん長いコードになってしまいましたが、大部分はもとの CakeEmail クラスからのコピペです。やったことは次のとおり:

  1. CakeEmail クラスから、Validation::email() を呼んでいるメソッドを検索する
  2. 検索して見つかった CakeEmail::_setEmail() と CakeEmail::_addEmail() をまるまるコピペする
  3. Validation::email() を呼んでいる箇所(合計4箇所)を、独自のバリデーションメソッド CakeEmailNormalized::validateEmail() を呼ぶよう変更する
  4. 独自のバリデーションメソッド CakeEmailNormalized::validateEmail() を実装する

バリデーションメソッドのアルゴリズムは、先述の参考記事にならっています。コールバック関数をどうするかちょっと迷ったのですが、次のようなことを考えて上のようになりました。

  • 無名関数を使うのがいちばんスマートそうだけど、無名関数が実装されていない PHP5.2 を CakePHP 本体がサポートしている以上、迂闊に手を出せない
  • かといって、create_function() を使うとメソッドを呼び出すたびに関数が定義されてメモリリークが怖い。メールマガジンの配信システムなんかで何万件というメールアドレスを一度に処理しても心配ないようにしたい
  • コールバック用のメソッドを立ててしまおうか…名前空間を汚してしまうけど、クラス内ローカルならいいよね

71行でやっている、オブジェクトのメソッドをコールバックに指定する方法はポイントかもしれません。これを知っておくと、名前空間や無名関数を使わなくても(≒ PHP5.2 以下の環境でも)グローバルな名前空間を汚さずコールバック関数を定義することができるようになります。個人的には、WordPress でフックをがんがん使うようなカスタマイズをする際によくお世話になります。が、世の中の PHP のバージョンが上がればまた状況が変わってくるかもしれません。

余談:ここまでする?

実装を模索するうち楽しくなってきちゃったのでここまでやってしまいましたが、やはり個人的にはメールアドレスのバリデーションに凝るのは骨ばかり折れて得るところが少ないと感じています。そもそも CakeEmail クラスの中でバリデーションなんてしてくれなきゃこんなことには…。

そんなわたしの気持ちにフィットする論説を kimoto に教えてもらったので結びにご紹介しておきます。

  • このエントリーをはてなブックマークに追加

この記事を読んだ人にオススメ