mk-mode BLOG

このブログは自作の自宅サーバに構築した Debian GNU/Linux で運用しています。
PC・サーバ構築等の話題を中心に公開しております。(クローンサイト: GitHub Pages

ブログ開設日2009-01-05
サーバ連続稼働時間
Reading...
Page View 合計
Reading...
今日
Reading...
昨日
Reading...

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

[ プログラミング ] [ Mail, MySQL, Ruby ]

こんばんは。

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

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

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

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

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

1
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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 では文字列を比較チェックするより断然「正規表現」でのマッチングが高速だし。。。

以上。

Comments