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

Updated:


先日は、Ruby でメールの UNIX MBOX データの読み込みを試してみました。

今後、この読み込んだデータを MySQL に保存することを考えていますが、何万件とあるデータを一気に取り込もうとすると少なからず不正なデータ存在します。

そこで、少しずつ Ruby でデータの検証をしてみようと考えました。

今回は、メールヘッダの「Date」属性を検証してみました。

このメールヘッダの「Date」は「RFC 5322」(以前の「RFC 2822」・「RFC 822」)に準拠した書式でなければなりません。

Fri, 14 Oct 2011 23:59:59 +0900

というような書式です。

参考サイト

※「RFC 822」の改訂版が「RFC 2822」で「RFC 2822」の改訂版が「RFC 5322」ですが、今でも「RFC 822」や「RFC 2822」に準拠した記述をしているものがあるようです。(実際、多数あります) ※「RFC」の詳細はWeb等で検索してみてください。

補足すると、

  • 曜日部分 “Fri, “ はなくてもよい。
  • 秒部分 “:59” はなくてもよい。
  • タイムゾーン部分は “+0900” というような “+” か “-“ と4桁の数字でなく、 “UT”, “GMT”, “EST”, “EDT”, “CST”, “CDT”, “MST”, “MDT”, “PST”, “PDT”, “Z”, “A”, “M”, “N”, “Y” でもよい。( RFC 822 準拠の場合 )

この「Date」属性が「RFC 5322」(「RFC 2822」・「RFC 822」)に準拠した書式となっているかどうかを「正規表現」を使用してチェックしてみました。

Rubyサンプルスクリプト

Ruby - UNIX MBOXデータ読み込み!」で紹介したRubyスクリプトを流用しています。 以下のスクリプトでは今後のために正規表現でのマッチング処理時に値を取得できるようにしています。 ●ファイル名:ana_mbox_check_date.rb

require 'find'
require 'kconv'
require 'time'

class AnaMboxCheckDate

  # MBOXディレクトリ
  DIR_MBOX = "D:\01_Mail\Thunderbird"
  
  # 正規表現 ( Date )
  REG_DATE = /(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun), )?(\d{1,2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}|\d{2}) (\d{2}):(\d{2})(?::(\d{2}))? (UT|GMT|[ECMP][SD]T|[ZAMNY]|[+-]\d{4})/
  
  # [CLASS] 解析
  class Analyze

    # INTIALIZE
    def initialize
      
      # 読み込みシーケンスNo
      @seq = 0
      # 正規表現にマッチしないものの件数
      @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*/, '' )
                if @attr == "Subject"
                  # Subjectの場合は改行せずに結合
                  header[@attr] += line
                else
                  header[@attr] += "\n" + line
                end
              end

            end

            # 読み込みSEQインクリメント
            @seq += 1
            
            # nilなら""を設定
            header['Date']    ||= ""
            header['Subject'] ||= ""

            # 以下の正規表現マッチング処理で取得できるもの
            # $1 : 曜日, $2 : 日 $3 : 月, $4 : 年, $5 : 時, $6 : 分, $7 : 秒, $8 : タイムゾーン
            unless header['Date'] =~ REG_DATE
              @cnt_notmatch += 1
              puts "SEQ.#{@seq}"
              puts "\tDate   : #{header['Date']}"
              puts Kconv.tosjis( "\tSubject: #{header['Subject']}" )
            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 seq
    
      return @seq
      
    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 "NO REGEXP   = #{obj_ana.cnt_notmatch}"
    puts "====< E N D >===="

  rescue => e

    # エラーメッセージ
    str_msg = "[EXCEPTION] " + e.to_s
    STDERR.puts( str_msg )
    exit 1

  end

end

検証結果

当方のメールデータ(Thunderbird)で検証した結果、「RFC 2822」 (「RFC 822」)に準拠していなかったものは、61,655件中 22件 でした。 内訳は以下のとおり。

1.時刻の「時」部分が2桁でなく1桁のもの 17件
2.「日」と「月」の間に半角空白が2個あるもの 2件
3.「Date」属性値がないもの 3件

それほど、深刻なものではありませんでした。 1と2は Time.parse で問題なく変換できます。 3は何かしら値を設定してやればよいです。

参考書籍


今回は、メールヘッダの「Date」属性をチェックしましたが、次回も何か検証するつもりです。

それにしても「正規表現」でのマッチング処理って面白いし、便利ですね。

Ruby では文字列を比較チェックするより断然「正規表現」でのマッチングが高速だし。。。

以上。





 

Sponsored Link

 

Comments