Ruby で メールアドレスの有効性チェックしてみた

# はじめに
C 向けのサービスを運営していると、ログインID や連絡のためにメールアドレスを取得することが多い。

で、その取得したメールに対して、大量のメール配信を行ったところ、バウンス率が高いために Amazon SES から配信停止を食らったもんで、
停止解除のための説得材料として、登録されているメールアドレスの有効性チェックを行おうと、方法を色々調べた。

下記のように Web で同様のサービスもやってますが、まぁ csv インポートだとお金かかったりだとか、
色々あったので、調べて自分で実装してみました。まぁざっと作ったのでひどいソースですが…

http://www.verifyemailaddress.org/
http://address-kakunin.com/
https://www.voilanorbert.com/

メールアドレスの有効性をチェックするには、telnet で該当のメールの SMTP サーバーに接続し、
STMP の手順通りにコマンド実行後、送信先を設定する `RCPT TO:` を実行すると、
その時点で、そのメールアドレスが存在するか否かをステータスコードと共にメッセージを返却するので、
ステータスコードで、チェックできることを突き止めた。

[SMTP 応答コード](http://www.puni.net/~mimori/smtp/reply.html)

## telnet 実行例
```
$ telnet mail.hogehoge.com 25
Trying 10.0.0.1...
Connected to mail.hairmo.jp.
Escape character is '^]'.
220 hogehoge.com ESMTP unknown
EHLO host.example.jp               # EHLO コマンドを実行。そのSMTPサーバーが対応するサービスが返却される
250-hogehoge.com
250-PIPELINING
250-SIZE 10240000
250-ETRN
250-STARTTLS
250-AUTH PLAIN LOGIN
250-AUTH=PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
mail from:<hogehoge@hogehoge.com>  # MAIL FROM コマンド実行。from のアドレスを設定
RCPT TO:<hogehoge@hogehoge.com>    # RCPT TO コマンドを実行。ここで宛先を指定
250 2.1.5 Ok                       # 250 が返却されると、そのメールアドレスは存在している
RCPT TO:<test@hogehoge.com>        # 存在しないアドレスを指定すると 550 が返る
550 5.1.1 <tes@hairmo.jp>: Recipient address rejected: User unknown in local recipient table
```

## 完成までの道のり…

なので、`net/telnet` で上記手順を実装してみたのだが、うまく SMTP サーバーの入力待ち (プロンプト) をフックできず、諦めた。

試した内容は、telnet 先サーバーの入力待ちを、Prompt の正規表現 `/\A\d{3}\s+/` で判断させたのだが、
telnet をスタートさせた段階で、既にメッセージが返ってきており (上記 telnet 時の 220 の行)、その後 `HELO` コマンドを実行すると、
対応するメッセージが上記 telnet 時の `250-hogehoge.com` から `250 DSN` にも関わらず、
`220 hogehoge.com ESMTP unknown` を取得してい、最後に宛先に登録した時 ( RCPT TO ) のメッセージが取得できなかった。

その次に、TCPSoket を使用してみたが、STMP サーバーから返ってくるメッセージを gets で取得したが、
次のメッセージがない場合にタイムアウトになってしまっていて、うまいことメッセージの取得が出来なかったので、
またまた諦めた。

最終的に、`net/smtp` を使った。
ざっと調べた時に、上位の層のモジュール化と思ってスルーしていたんだけど、調べ直したら使えることがわかった。
下記にサンプルソースを記載。

# サンプルソース

```rb:varifay_email_address.rb
require 'resolv'
require 'net/smtp'

if ARGV.count != 2
  puts "argument error command: #{__FILE__} read_file_name write_file_name"
  exit 1
end

r_filename = ARGV.first
w_filename = ARGV.last
VALID_ADDR_REGEXP = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/
results = []
File::open(r_filename, 'r') {|f|
  f.each {|line|
    email = line.chomp
    next if email.empty?
    if email.match(VALID_ADDR_REGEXP).nil?
      results << {email: email, domain: false, result: "email is invalid"}
      next
    end
    domain = email.split("@").last
    records = Resolv::DNS.open do |dns|
      dns.getresources(domain, Resolv::DNS::Resource::IN::MX)
    end

    unless records.empty?
      begin
        Net::SMTP.start(records.first.exchange.to_s, 25, 'example.com')do |smtp|
          smtp.mailfrom("example@example.com")
            res = smtp.rcptto(email)
            results << {email: email, domain: true, result: res.string.chomp}
        end
      rescue # アドレスが存在しない場合は例外が発生
        results << {email: email, domain: true, result: $!.message.chomp}
      end
    else
      results << {email: email, domain: false, result: "domain not exists"}
    end
  }
}

f = File.open(w_filename, 'w')
f.puts "email,domain,result" # header
results.each do |line|
  f.puts "\"#{line[:email]}\",\"#{line[:domain]}\",\"#{line[:result]}\"" # body
end
f.close
```

一応、Ruby 1.9.3 以降では動きました。

# サンプルの説明
引数に、メールアドレスのリスト(メールアドレスだけが記載されたリスト) と、結果の CSV の出力ファイル名を与えて実行。

処理内容としては、ファイルを 1 行ずつ読んで、取り出したメールアドレスをざっくりフォーマットチェックして、
ドメインドメイン部分を取得。
更にそのドメインから DNS で MX レコードを取得して、SMTP のコマンドを順序良く実行して、`RCPT TO `の結果を `Hash` で、`Array` に突っ込んで、最後に、結果を CSV で出力してみた。

# 実行上の注意点
- スクリプトを実行するサーバーは、逆引き設定しておくこと  
逆引き出来ないサーバーで実行すると、サーバーによっては、`450 4.7.1 Client host rejected: cannot find your hostname, [10.0.0.1]` と言ったエラーが返るので、正常にアカウントのチェックが出来ない。
なので、このスクリプトは、ローカルの PC とかからは実行不可。  
- 大量のメールアドレスのリストを読み込ませない  
このスクリプトで最初に 5000 件やってみたら、30 分以上音沙汰なかったので、`print` でメールアドレスと、読込件数等を出力して、1 回の実行では 1000 件程度に絞ったほうがいいかも。1000 件だと数分で終わるはず。
- au のアドレスに気をつけろ！！！！！  
当方の作業時、au からは、全て存在する旨の `250` が返ってきた。しかしながら、実際送信すると `return` 。。。。  
たぶん、au 側で、全て 250 で返してるんだと思います。  
なので、ezweb のアドレスには有効な手段ではありません。  
- docomo のアドレスにも気をつけろ！！！！！
docomo のメールサーバーは、同じ IP から大量の STMP セッションが張られた場合、一定時間、ブロックするみたいで、途中まで、`250` とか `550` が返ってきていて、その後いきなり `421 Service not available, closing transmission channel` が返るようになります。
なので、ちゃんと docomo のアドレス確認したい場合は、数件ずつやったほうがいいです。
携帯アドレス向けのメール配信とかでも、それを回避するために 複数の SMTP サーバーを使って配信したりしてるみたいです。

# まとめ
telnet すれば色々できる。( POP も telnet で出来るよ )
メールサーバーによっては、有効性チェックできないものや、そもそもセッションがリジェクトされるサーバーがある。
けど、チェックすることで、バウンス率はかなり低下できる。データクレンジングの手段として有効だと思う。
　メール認証していなかったアドレスに対して、メール配信した際、当初 20% 弱のバウンス率が、スクリプト実行後のリストで **4%** 弱まで低下。
　※ バウンスのアドレスの 95% 以上が注意点で記載した ezweb だったので、それがなければ、1%以下
**Amazon SES は、止められると再開させてもらうのに英語で対応内容等の情報を詳細に出さなくてはならないので、めんどくさいので、運用に気を使う。**