mk-mode BLOG

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

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

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

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

こんばんは。

先日は、Ruby でメールの UNIX MBOX データのの Date 属性を検証してみました。

引き続いて今回は、Ruby でメールヘッダの「From」属性を検証してみました。

From属性にはメールの送信者(実質的には作成者)がメールアドレス+αで設定されているはずです。 大抵は1件が設定されていますが、作成者は1人とは限らず、複数設定されているケースもまれにあります。 複数設定は正常なので問題ありませんが、Fromが設定されていないものがあります。

また、設定されるメールアドレスは「Date」同様「RFC 5322」(「RFC 2822」・「RFC 822」) に準拠した書式でなければなりません。 参考サイト

※RFCの詳細はWeb等で検索してみてください。

この「From」属性のメールアドレスが「RFC 5322」に準拠した書式となっているかどうかを「正規表現」を使用してチェックしてみようと思いましたが、正確にチェックするには非常に複雑なので、ある程度のチェックにとどめました。 大抵はプロバイダ側で制限しているので問題になるようなアドレスは設定されないはずです。 正確に正規表現でチェックしようと思ったら、こちら「メールアドレスの正規表現」を参考にされるとよいと思います。(あまりの長さにびっくりすると思います)

今回は以下のような「From」属性のグループ分けとこれらにマッチしないもの、「From」属性が設定されていないものをチェックしてみます。

1
2
3
4
5
6
7
8
9
10
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

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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
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

検証結果

当方で検証した結果は以下のとおり。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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