Ruby - UNIX MBOX メールヘッダ「From」検証!

更新日時:


先日は、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:&#92;&#48;1_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」属性等にも応用が可能です。 今後随時チェックしてみようと思います。

以上。

 

Sponsored Link

 

コメントする