RubyでOutlook Expressのdbxファイルを読み、spamの統計をとってみる実験
新年明けましておめでとうございます。
俺にとって、去年、2006年は、あまりいい年ではなかったように思う。なんというか、つまらなかった。
年々面白いことが減っていくような気がする。なにもかもがつまらなく思えてしまうようになった。
その一方で、鬱陶しいことはどんどん増えていく。
その「鬱陶しいこと」のひとつが、迷惑メール、spamだ。
去年は、なぜかしらないけど、迷惑メールの量がどんどん増えていって、一昨年ぐらいまでは、月に数通しかこなかった迷惑メールが、驚くほど大量に届くようになった。
そのspamメール、普通の人はすぐに削除しちゃうんだろうけど、俺は全部保存しておいた。あとで数えてみようと思ったのだ。
迷惑メールは全部Outlook Expressに保存してある。なので、保存したメールはdbxという拡張子のファイルに保存されている。
それを自作のRubyスクリプトで読み出して、全てのメールの送信時刻をとりだし、グラフプロットソフト gnuplot でプロットしてみた。
まずこれは10日ごとにspamの数を数えてグラフにしたもの。ちなみに去年一年のspam総数は7467通。12月には、10日で500通くらいくるようになった。
さらにこれは、時間帯ごとに何通来ているか調べたもの。
なんでこんなグラフをかいてみたかというと、どうも、夜中に来る迷惑メールのほうが日中来るのより、多いような気がして、それを確認してみたかったのだ。
うちに来るspamは、ほぼ全て英語のものなんだけど、どういう理由でこういう差がでるのかは、わからない。
たぶん、こういう迷惑メールはウイルス感染した一般の人のパソコンから送信されているじゃないかと思うけど、そういうパソコンが活発に活動している時間帯は、やや多くなるんではないかと。
たぶん、日本の正午頃、明け方になるようなところ(ヨーロッパ?)からきているものが多い……のだろうか?
ちなみに、OEのdbxを読み出すのに使ったRubyについてだけど。
dbxファイルのフォーマットを調べるのに役立ったのが、このサイト。
(偉そうなこといえる立場じゃないんだけど、このサイト、英語が微妙に独特な言い回しになっている)
あと、CPANでみつけた「Mail-Transport-Dbx」っていうPerlモジュールに入っていた、「libdbx」っていうののソースコードと解説書。これも役に立った。
Rubyは、全然初心者なのでちょっと恥ずかしいんだけど、とりあえず、スクリプトもおいときます。
DBXクラスをnewすると、配列@message_posに各メールのメタ情報の場所(seekする場所)が入る。メタ情報を読み取るのがMessage_infoクラスで、これの"case idx"のところを、ここをみながら補完していくと、より完璧になる。(今回は、時間情報だけ必要だったので、それしか取ってない)
あと、Message_info::get_messageが、メール本体を取り出すところ。
require 'win32/registry' class Message_info attr_reader :sent_time attr_reader :save_time attr_reader :rcvd_time attr_reader :message def initialize(dbx, pos) ftype = %w(L L Q L L S Q S S S S S S S S S L L Q S S ) f = dbx.dbx_file f.seek(pos) a = f.read(0xc).unpack("L2SC2") bodylen = a[1] entries = a[3] body = f.read(bodylen) indexes = body.unpack("C4" * entries) data = body[(entries*4),body.length-entries*4] x = [] (0..(entries-1)).each do |i| tmp = indexes[(i*4+1)..(i*4+3)] tmp.push(0) x[i] = tmp.pack("C4").unpack("L")[0] end x.push(data.length) (0..(entries-1)).each do |i| idx = indexes[i*4] v = '???' if idx >= 128 idx -= 128 v = x[i] else v = data[x[i]..(x[i+1]-1)] case ftype[idx] when "L" v = v.unpack("L")[0] when "Q" v = v.unpack("Q")[0] end end case idx when 2 @sent_time = (Win32::Registry.wtime2time(v) rescue 0) when 6 @save_time = (Win32::Registry.wtime2time(v) rescue 0) when 18 @rcvd_time = (Win32::Registry.wtime2time(v) rescue 0) when 4 # @message = self.get_message(dbx, v) end if idx != 0x1c #print "%0x: %d %s\n" % [idx, i, v] end end end def get_message(dbx, pos) f = dbx.dbx_file msg = '' until pos == 0 do f.seek(pos) a = f.read(16).unpack("L4") bodylen = a[2] nextpos = a[3] msg << f.read(512)[0, bodylen] pos = nextpos end return msg end end class DBX attr_accessor :message_pos attr_accessor :message attr_accessor :dbx_file def initialize(filename) @dbx_file = File.open(filename, 'rb') @file_header = @dbx_file.read(0x24bc).unpack("L*") @root_node = @file_header[0x39] @message_pos = [] self.readtree(@root_node) @message = [] n = 1 @message_pos.each do |i| @message.push(Message_info.new(self, i)) # print "%d\r" % n n += 1 end end def readtree(pos, level=1) # print "level: %d\n" % level @dbx_file.seek(pos) tree_header = @dbx_file.read(0x18) a = tree_header.unpack("L4C2sL") child_tree = a[2] entries = a[5] tree_body = @dbx_file.read(12 * entries).unpack("L3" * entries) if child_tree != 0 # print "child found in head (%d)\n" % a[7] self.readtree(child_tree,level+1) end # print "%d entries\n" % entries (0..(tree_body.length-1)).step(3) do |i| child_tree = tree_body[i+1] if tree_body[i+1] != 0 # print "child found in body (%d)\n" % tree_body[i+2] self.readtree(child_tree, level+1) end @message_pos.push(tree_body[i]) end end end dbxfile = 'inbox.dbx' d = DBX.new(dbxfile) d.message.each do |m| p m.rcvd_time end print "%d messages\n" % d.message_pos.length