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ファイルのフォーマットを調べるのに役立ったのが、このサイト。

http://oedbx.aroh.de/

(偉そうなこといえる立場じゃないんだけど、このサイト、英語が微妙に独特な言い回しになっている)

あと、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