gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

メールアドレスの確認(本当の本題:拡張アドレスの除去方法)

とりあえず「拡張アドレスの拡張部分を除去」が命題になるのですが…もうちょっと正しく書くとおそらくは違って、さらっとぶっちゃけると「DBでprimary keyだったりUNIQUE制約かかってたりするのにちゃんと引っかかる」状況を作り出したい、ってのが、おそらく本筋。
で…ここから少し面倒なお話し。


とりあえず標的に上がってるのは「+(プラス)」記号で、これが割と拡張アドレスとしてつかわれ「やすい」。
ただ、拡張アドレスはqmailだと「−(ハイフン)」だし、Postfixだと「設定によっては+も−も使える(Postfix 2.11以降)」。一方で「普通にlocal-partの文字として−(ハイフン)が使える」所もあるので、色々と悩ましい。
それ以外にも「quoted string(ダブルクォートで囲った文字列)をlocal-partにする」ってやり方もあってこれも割とあちこちで使えるし(斜め調査の限りでは、AUスマホの「quoted stringアドレスへの送信」のみNGで、後は送受信とも割といける)。
あと、これはgmail以外では見ていないんだけど「Gmail ではユーザー名のピリオドを文字として認識しません( https://support.google.com/mail/answer/10313?hl=ja )」という謎仕様もあるので、ドット位置変えると割と色々いける(ドットの連続はどうもGoogle側で(も?)弾いてるぽい。「Final-Recipient: rfc822; "h..oge"@gmail.com」ってエラーがきた)。


この辺を考えると、細かくいくと「ドメイン毎に処理を変える」必要が出てきそうではあるんだけど。
大まかな「local-partからいらんものを引っぺがす!」ってのを前提に
・前後のダブルクォートは、あったら引っぺがす
・domain-partの文字は(小文字側に)揃える。local-partは「区別したりしなかったり」だと思うので、一旦「小文字に揃える」をデフォに。「小文字に揃えない(大文字と小文字を区別するMTA(RFC822準拠、RFC5321非準拠?)」の可能性が0ではないので、ギミックだけ作って運用対処としておく。
・+以降は「拡張アドレスの可能性が高い」ので、+以降を引っぺがす(後で「引っぺがさないドメインリスト」とか作って運用で足し込む)
・−以降は「拡張アドレスの可能性がある」ので、−以降を引っぺがす準備をする(「引っぺがすドメインリスト」とか作って運用で足し込む。初期は空でいいのかも)
Googleアカウントについては「ドットを消す」:いるかなぁ? とも思うんだけど、可能なのでせっかくだしやっとく。これも「引っぺがすリスト」作って、後で追加対応にしておくといいのかなぁ?


こんな感じ。
ちなみに前提として「アドレスは到達する」事を期待しているので、ドメイン周りは未着手。
ドメイン周りに手を出したい場合、最終的には「DNS引くよねぇ」って話になるので、それはまた「やるならやって」的なところw


文章書きながらで実装3時間ちょい…うち1時間弱が「調査+書き物」、1時間が「コーディング」、1時間がテスト。
テストの大半は「テストデータの用意ミス」という切ないっぷり(苦笑
とりあえず「さほどコストかからずに書けるよ」って程度なので、一旦「プロトタイプ的なブツだけどこんなもんぢゃね?」程度のものを。


っつわけで、コード(+下の方に簡単なテスト付き)

<?php

// 拡張アドレスっぽいもの除去クラス
class extended_address_removal {

// 本当は「外部ファイルから読み込み」とかで適切に読み込んでくだせぇ(苦笑
public function __construct() {
  //
  $this->not_lower_list([
    'not-lower-example.com',
  ]);
  //
  $this->plus_unremove_list([
    'plus-unremove-example.com',
    'hyphen-only-remove-example.com', // XXX ここ「二重登録」だなぁ…直感的じゃない…後で変えたほうがいいかも:これは「ハイフンのみ除去対象、+は生かす」設定用
  ]);
  //
  $this->hyphen_remove_list([
    'hyphen-only-remove-example.com', // XXX ここ「二重登録」だなぁ…直感的じゃない…後で変えたほうがいいかも:これは「ハイフンのみ除去対象、+は生かす」設定用
    'hyphen-remove-example.com', // こっちだと「ハイフンもプラスも除去する」になる
  ]);
  //
  $this->dot_remove_list([
    'gmail.example.com',
  ]);
}

// 設定する系メソッド
// 検索しやすいようにarray_flipしてるのでほんのり注意
public function not_lower_list($awk) {
  $this->not_lower_list_ = array_flip($awk);
}
public function plus_unremove_list($awk) {
  $this->plus_unremove_list_ = array_flip($awk);
}
public function hyphen_remove_list($awk) {
  $this->hyphen_remove_list_ = array_flip($awk);
}
public function dot_remove_list($awk) {
  $this->dot_remove_list_ = array_flip($awk);
}

// 除去本体
public function remove($email) {
  // local-partとdomain-partに切り分ける
  $awk = explode('@', $email);
  // 万が一「@無しの文字列」が入ってきた時用
  // XXX このチェックいるか?
  if (1 === count($awk)) {
    return '';
  }

  //
  $domain = array_pop($awk);
  $local = implode('@', $awk);
//var_dump($local);
//var_dump($domain);

  // domain-partの文字は(小文字側に)揃える。local-partは「区別したりしなかったり」だと思うので、一旦「小文字に揃える」をデフォに。「小文字に揃えない(大文字と小文字を区別するMTA(RFC822準拠、RFC5321非準拠?)」の可能性が0ではないので、ギミックだけ作って運用対処としておく。
  if (false === isset($this->not_lower_list_[$domain])) {
    $local = strtolower($local);
  }

  // 前後のダブルクォートは、あったら引っぺがす
  // XXX 不完全なquoted stringは一旦弾こう
  $flg = ('"' === $local[0]); // 先頭の"チェック
  $flg2 = ('"' === $local[strlen($local) - 1]); // 最後の"チェック
  // 「どっちかだけtrue」は不完全なquoted stringなのでエラー
  if ($flg xor $flg2) {
    return '';
  }
  // quoted stringなら除去処理
  if (true === $flg) {
    $local = substr($local, 1, -1);
  }
//var_dump($local);
//var_dump($domain);

  // ・+以降は「拡張アドレスの可能性が高い」ので、+以降を引っぺがす(後で「引っぺがさないドメインリスト」とか作って運用で足し込む)
  if (false === isset($this->plus_unremove_list_[$domain])) {
    $awk = explode('+', $local);
    $local = $awk[0];
  }

  // ・−以降は「拡張アドレスの可能性がある」ので、−以降を引っぺがす準備をする(「引っぺがすドメインリスト」とか作って運用で足し込む。初期は空でいいのかも)
  if (true === isset($this->hyphen_remove_list_[$domain])) {
    $awk = explode('-', $local);
    $local = $awk[0];
  }

  // ・Googleアカウントについては「ドットを消す」:いるかなぁ? とも思うんだけど、可能なのでせっかくだしやっとく。これも「引っぺがすリスト」作って、後で追加対応にしておくといいのかなぁ?
  if (true === isset($this->dot_remove_list_[$domain])) {
    $local = str_replace('.', '', $local);
  }

  //
  return $local . '@' . $domain;
}

//private:
private $not_lower_list_;
private $plus_unremove_list_;
private $hyphen_remove_list_;
private $dot_remove_list_;
} // end of class

/*
 * 以下テスト
 */

// テスト用アドレス群
$add = [
  // ノーマルなパターン
  'nobody@example.com' => 'nobody@example.com',
  'NObody@example.com' => 'nobody@example.com', // local-partは小文字に揃える
  'no+body@example.com' => 'no@example.com', // 拡張なので除去
  'no-body@example.com' => 'no-body@example.com', // ハイフンはデフォでは除去しない
  'no.body@example.com' => 'no.body@example.com', // .(dot)はデフォでは除去しない
  '"no+body"@example.com' => 'no@example.com', // (quoted string)拡張なので除去
  '"no-body"@example.com' => 'no-body@example.com', // (quoted string)ハイフンはデフォでは除去しない
  '"no.body"@example.com' => 'no.body@example.com', // (quoted string) .(dot)はデフォでは除去しない
  '"no.body@example.com' => '', // 不完全なquoted string
  'no.body"@example.com' => '', // 不完全なquoted string
  '"nob@ody"@example.com' => 'nob@ody@example.com', // quoted stringの除去

  // local-partを小文字にしない
  'NObody@not-lower-example.com' => 'NObody@not-lower-example.com',

  // プラスを除去しない
  'no+body@plus-unremove-example.com' => 'no+body@plus-unremove-example.com',
  '"no+body"@plus-unremove-example.com' => 'no+body@plus-unremove-example.com',

  // ハイフンを取り除き、プラスは除去しない
  'no-body@hyphen-only-remove-example.com' => 'no@hyphen-only-remove-example.com',
  '"no-body"@hyphen-only-remove-example.com' => 'no@hyphen-only-remove-example.com',
  'n+o-body@hyphen-only-remove-example.com' => 'n+o@hyphen-only-remove-example.com',
  '"n+o-body"@hyphen-only-remove-example.com' => 'n+o@hyphen-only-remove-example.com',

  // ハイフンもプラスも取り除く
  'no-body@hyphen-remove-example.com' => 'no@hyphen-remove-example.com',
  '"no-body"@hyphen-remove-example.com' => 'no@hyphen-remove-example.com',
  'n+o-body@hyphen-remove-example.com' => 'n@hyphen-remove-example.com',
  '"n+o-body"@hyphen-remove-example.com' => 'n@hyphen-remove-example.com',

  // dotを取り除く
  'no.b.ody@gmail.example.com' => 'nobody@gmail.example.com',
  'no.b.ody+test@gmail.example.com' => 'nobody@gmail.example.com',
  '"no.b.ody"@gmail.example.com' => 'nobody@gmail.example.com',
  '"no.b.ody+test"@gmail.example.com' => 'nobody@gmail.example.com',
];

//
$error_count = 0;
$obj = new extended_address_removal();
foreach($add as $email => $expectation_email) {
  $r = $obj->remove($email);
  if ($r === $expectation_email) {
    //echo "ok\n";
    echo "ok: {$email} => {$r}\n";
  } else {
    echo "ng orz\n";
    //echo "{$email} => {$r} !: {$expectation_email}\n";
    $error_count ++;
  }
}
if (0 === $error_count) {
  echo "...................ALL OK!!\n";
} else {
  echo "................NG( {$error_count} )!!\n";
}

とりあえずこのクラス使うと「なんとなし拡張っぽいものを一通り引っぺがせる」ので。
実装するemailアドレスは別カラムにちゃんと保存しておいてもらうとして、こいつで引っ張ってきたアドレスをUNIQUE制約したりしておくと、当初目的である
・拡張アドレスは認める
・重複したアドレスは認めない
が、ある程度いけるんではないかなぁ、と。
あとは運用でドメインを追加したりすれば、まぁまぁいけるんじゃないかなぁ、って思うざます。


…多分、そのうちMagicWeaponに追加するような気がするw