Ruby - UNIX MBOX メールヘッダ「From」検証!
Updated:
先日は、Ruby でメールの UNIX MBOX データのの Date 属性を検証してみました。
引き続いて今回は、Ruby でメールヘッダの「From」属性を検証してみました。
From属性にはメールの送信者(実質的には作成者)がメールアドレス+αで設定されているはずです。 大抵は1件が設定されていますが、作成者は1人とは限らず、複数設定されているケースもまれにあります。 複数設定は正常なので問題ありませんが、Fromが設定されていないものがあります。
また、設定されるメールアドレスは「Date」同様「RFC 5322」(「RFC 2822」・「RFC 822」) に準拠した書式でなければなりません。 参考サイト
※RFCの詳細はWeb等で検索してみてください。
この「From」属性のメールアドレスが「RFC 5322」に準拠した書式となっているかどうかを「正規表現」を使用してチェックしてみようと思いましたが、正確にチェックするには非常に複雑なので、ある程度のチェックにとどめました。 大抵はプロバイダ側で制限しているので問題になるようなアドレスは設定されないはずです。 正確に正規表現でチェックしようと思ったら、こちら「メールアドレスの正規表現」を参考にされるとよいと思います。(あまりの長さにびっくりすると思います)
今回は以下のような「From」属性のグループ分けとこれらにマッチしないもの、「From」属性が設定されていないものをチェックしてみます。
0 : "xxxx" <xxx@xxx.xxx>
1 : "" <xxx@xxx.xxx>
2 : xxxx <xxx@xxx.xxx>
3 : <xxx@xxx.xxx>
4 : "xxxx" <xxx@xxx.xxx> xxxx
5 : "" <xxx@xxx.xxx> xxxx
6 : xxxx <xxx@xxx.xxx> xxxx
7 : <xxx@xxx.xxx> xxxx
8 : xxx@xxx.xxx
9 : xxx@xxx.xxx xxxx
Rubyサンプルスクリプト
Ruby - UNIX MBOX メールヘッダ「Date」検証!で紹介したRubyスクリプトを流用しています。 前述のグループ分けの0~7用の正規表現と8・9用の正規表現を使用しています。 ●ファイル名:ana_mbox_check_from.rb
require 'find'
require 'kconv'
class AnaMboxCheckFrom
# MBOXディレクトリ
DIR_MBOX = "D:\01_Mail\Thunderbird"
# 正規表現 ( From )
# (一般的なメールアドレス正規表現を使用する場合)
# REG_FROM_1 = /^(.*)<([\w\-\.]+@[\w\-\.]+\.[a-zA-Z]+)>(.*)?$/
# REG_FROM_2 = /^([\w\-\.]+@[\w\-\.]+\.[a-zA-Z]+)(.*)?$/
# (上記を更に改良したバージョン)
REG_FROM_1 = /^(.*)<([\w+-=?^-~]+(?:\.[\w+-=?^-~]+)*@[-\w]+(?:\.[-\w]+)*\.[a-zA-Z]+)>(.*)?$/
REG_FROM_2 = /^([\w+-=?^-~]+(?:\.[\w+-=?^-~]+)*@[-\w]+(?:\.[-\w]+)*\.[a-zA-Z]+)(.*)?$/
# [CLASS] 解析
class Analyze
# INTIALIZE
def initialize
# 読み込みシーケンスNo
@seq = 0
# Fromが非設定のものの件数
@cnt_non = 0
# 正規表現にマッチしたものの件数
@cnt_match_0 = 0 # "xxxx" <xxx@xxx.xxx>
@cnt_match_1 = 0 # "" <xxx@xxx.xxx>
@cnt_match_2 = 0 # xxxx <xxx@xxx.xxx>
@cnt_match_3 = 0 # <xxx@xxx.xxx>
@cnt_match_4 = 0 # "xxxx" <xxx@xxx.xxx> xxxx
@cnt_match_5 = 0 # "" <xxx@xxx.xxx> xxxx
@cnt_match_6 = 0 # xxxx <xxx@xxx.xxx> xxxx
@cnt_match_7 = 0 # <xxx@xxx.xxx> xxxx
@cnt_match_8 = 0 # xxx@xxx.xxx
@cnt_match_9 = 0 # xxx@xxx.xxx xxxx
# 正規表現にマッチしないものの件数
@cnt_notmatch = 0
end
# Mailbox名一覧取得
def get_mailbox_list
begin
# 指定ディレクトリ配下のディレクトリ一覧
res = Array.new
Dir::entries( DIR_MBOX ).each do |d|
if File::ftype( DIR_MBOX + "/" + d ) == "directory"
res << d unless ( d.to_s == "." || d.to_s == ".." || d.to_s == "local" )
end
end
return res
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION][" + self.class.name + ".get_mailbox_list] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
# MBOXファイル一覧取得
def get_mbox_list( mailbox )
begin
# 指定ディレクトリ配下のファイル一覧
res = Array.new
Find.find( DIR_MBOX + "/" + mailbox ) do |f|
# ファイルのみ抽出
unless File::ftype( f ) == "directory"
# ファイルを読み込み1行目の先頭がFromならMBOXと判定
open( f ) do |file|
l = file.gets
if l =~ /^From/
res << f
end
end
end
end
return res
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION][" + self.class.name + ".get_mbox_list] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
# MBOXファイル解析
def ana_mbox( mbox )
begin
# 1つのMBOXファイルを開く
open( mbox ) do |f|
f.slice_before do |line|
# "From "を1グループの先頭とみなす。
line.start_with? "From "
end.each do |mail|
header = {}
# HEADER読み込み
mail.each do |row|
line = row.kconv( Kconv::UTF8, Kconv::AUTO )
# 行最後の改行文字を削除
line.chop!
# "From "で始まる行を読み飛ばし
next if /^From / =~ line
# HEADERの終了
break if /^$/ =~ line
# 属性と値を取得
if /^(\S+?):\s*(.*)/ =~ line
( @attr = $1 ).capitalize!
header[@attr] = $2
elsif @attr
# 複数行にわたる場合は結合
line.sub!( /^\s*/, '' )
# From, Subjectの場合は改行せずに結合
if @attr == "From" || @attr == "Subject"
header[@attr] += line
else
header[@attr] += "\n" + line
end
end
end
# nilなら""を設定
header['Date'] ||= ""
header['Subject'] ||= ""
header['From'] ||= ""
# Fromを配列化
header['From'] = make_array( header['From'] )
# Fromが非設定のもの
if header['From'].length == 0
# 読み込みSEQインクリメント
@seq += 1
@cnt_non += 1
else
header['From'].each do |from|
# 読み込みSEQインクリメント
@seq += 1
# REG_FROM_1
if from =~ REG_FROM_1
# nil なら "" をセット
str_disp_name = ( $1 == nil ? "" : $1 )
str_mail_address = ( $2 == nil ? "" : $2 )
str_memo = ( $3 == nil ? "" : $3 )
# 前後の半角スペースを削除
str_disp_name = str_disp_name.gsub( /^\s/, "" ).gsub( /\s$/, "" )
str_mail_address = str_mail_address.gsub( /^\s/, "" ).gsub( /\s$/, "" )
str_memo = str_memo.gsub( /^\s/, "" ).gsub( /\s$/, "" )
# メールアドレスの後ろの備考が無い場合
if str_memo == ""
# 表示名にダブルクォーテーションが有る場合
if str_disp_name =~ /\"(.*)\"/
unless str_disp_name == ""
@cnt_match_0 += 1
else
@cnt_match_1 += 1
end
# 表示名にダブルクォーテーションが無い場合
else
unless str_disp_name == ""
@cnt_match_2 += 1
else
@cnt_match_3 += 1
end
end
# メールアドレスの後ろの備考が有る場合
else
# 表示名にダブルクォーテーションが有る場合
if str_disp_name =~ /\"(.*)\"/
unless str_disp_name == ""
@cnt_match_4 += 1
else
@cnt_match_5 += 1
end
# 表示名にダブルクォーテーションが無い場合
else
unless str_disp_name == ""
@cnt_match_6 += 1
else
@cnt_match_7 += 1
end
end
end
# REG_FROM_2
elsif from =~ REG_FROM_2
# nil なら "" をセット
str_disp_name = ""
str_mail_address = $1
str_memo = ( $2 == nil ? "" : $2 )
# 前後の半角スペースを削除
str_mail_address = str_mail_address.gsub( /^\s/, "" ).gsub( /\s$/, "" )
str_memo = str_memo.gsub( /^\s/, "" ).gsub( /\s$/, "" )
# メールアドレスの後ろの備考が無い場合
if str_memo == ""
@cnt_match_8 += 1
# メールアドレスの後ろの備考が有る場合
else
@cnt_match_9 += 1
#### DEBUG ####
# puts "SEQ.#{@seq} From: #{from}"
# puts "\tDisplayName : #{str_disp_name}"
# puts "\tMailAddress : #{str_mail_address}"
# puts "\tMemo : #{str_memo}"
#### DEBUG ####
end
# 上記以外
else
@cnt_notmatch += 1
puts "SEQ.#{@seq} From: #{from}"
end
end
end
end
end
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION][" + self.class.name + ".ana_mbox] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
# カンマ区切りを配列化
def make_array( str_from )
begin
# ダブルクォーテーション内に","が有る場合、
# splitで不具合を起こすので一旦"="に置き換える
str_from = str_from.gsub( /\"([^\"]*)\"/ ) do |match|
match.gsub( /,/, "=" )
end
# From複数存在に対応するため配列化
ary_from = str_from.split( ',' )
# "="を","に戻す
# 0.upto( ary_from.length - 1 ) do |i|
ary_from = ary_from.collect do |from|
from = from.gsub( /^\s/, "" ) # 先頭の空白を削除
from = from.gsub( /\s$/, "" ) # 最後の空白を削除
from = from.gsub( /\"(.*)\"/ ) do |match|
match.gsub( /=/, "," )
end
end
return ary_from
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION][" + self.class.name + ".make_array] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
def seq
return @seq
end
def cnt_non
return @cnt_non
end
def cnt_match( no )
case no
when 0
ret = @cnt_match_0
when 1
ret = @cnt_match_1
when 2
ret = @cnt_match_2
when 3
ret = @cnt_match_3
when 4
ret = @cnt_match_4
when 5
ret = @cnt_match_5
when 6
ret = @cnt_match_6
when 7
ret = @cnt_match_7
when 8
ret = @cnt_match_8
when 9
ret = @cnt_match_9
end
return ret
end
def cnt_notmatch
return @cnt_notmatch
end
end
##############
#### MAIN ####
##############
begin
puts "====< START >===="
# 解析クラスインスタンス化
obj_ana = Analyze.new
# Mailbox名一覧取得
mailbox_list = obj_ana.get_mailbox_list
# Mailbox分ループ
mailbox_list.each do |mailbox|
# MBOXファイル一覧取得
mbox_list = obj_ana.get_mbox_list( mailbox )
# MBOXファイル分ループ
mbox_list.each do |mbox|
# MBOXファイル解析
obj_ana.ana_mbox( mbox )
end
end
puts "TOTAL COUNT = #{obj_ana.seq}"
puts "COUNT Non = #{obj_ana.cnt_non}"
0.upto( 9 ) do |i|
puts "Match(#{i}) = #{obj_ana.cnt_match( i )}"
end
puts "Not Match = #{obj_ana.cnt_notmatch}"
puts "====< E N D >===="
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
検証結果
当方で検証した結果は以下のとおり。
SEQ.12514 From: WordPress <wordpress@192.168.11.3>
SEQ.12539 From: WordPress <wordpress@192.168.11.2>
SEQ.25101 From: mk-mode BLOG <wordpress@192.168.11.101>
TOTAL COUNT = 61689
COUNT Non = 4
Match(0) = 5856
Match(1) = 0
Match(2) = 33460
Match(3) = 358
Match(4) = 0
Match(5) = 0
Match(6) = 5
Match(7) = 0
Match(8) = 10729
Match(9) = 11274
Not Match = 3
この結果には表示されませんがメール件数は61,665件です。 4件に「From」属性が設定されておらず、正規表現にマッチしなかったものが3件という結果になりました。(→不正) また、27件( = 61,689 - ( 61,665 - 3 ) ) は何件かのメールに複数「From」属性が設定されていたということになります。(→正常) 今回マッチしなかったものはすべて当方のサーバで内部的に使用していたものでしたので、「From」属性が設定されているものでは不正なものはなかったということになります。 それ以前に、不正なメールはメールサーバやメーラでふるい落としているということでしょう。
参考書籍
今回は、メールヘッダの「From」属性をチェックしましたが、「To」属性や「Reply-to」属性、「Return-Path」属性等にも応用が可能です。 今後随時チェックしてみようと思います。
以上。
Comments