mk-mode BLOG

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

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

Ruby - UNIX MBOXデータ読み込み!

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

こんばんは。

Windows でメールを扱う場合、メールの保存形式は UNIX MBOX 形式にすることがあると思います。 当方も Thunderbird で UNIX MBOX 形式を使用しています。

この UNIX MBOX形式のデータを MySQL に保存してみたくて、まずは Rubyで 読み込んでみようと考えました。

Ruby1.8系であれば “mailread” や “tmail” ライブラリを使用するみたいですが、Ruby1.9系では標準では使用できないみたいです。

そこで “mail” ライブラリはどうかと思い調べてみましたが、メールの送受信系はこれで出来ますが、UNIX MBOX 形式データの解析については README を読んでも記述がなかったため出来ないと判断。

そこで “mailread” のソースを眺めてみたところ、数十行のソースだったし簡単に埋め込めそうだと思い、直接該当のRubyスクリプトに埋め込んでみました。

参考までに以下にRubyスクリプトを掲載します。 ※試験的に作成したものなので、細部で不具合は出るかと思います。なんとなく流れがわかればと・・・

UNIX MBOX データ読み込みRubyスクリプト

処理の流れ

  • 存在するメールボックスの一覧を取得。
  • 各メールボックス中のMBOXファイルの一覧を取得。
  • 各MBOXファイルを読み込む。

  • “From "で始まる行を1メールの先頭と判断。

  • 最初の空行でメールヘッダの終了を判断。
  • 各メールヘッダはハッシュに格納。
  • 以降、1メールの最後までをメールボディと判断。

※メールヘッダの属性値は1つずつしか保存できないので、"Received"のように複数あるものは最後の1件分が保存されます。(当方、今のところ"Received"は保存対象にしていないので問題はない) ※multipart や 添付ファイル のことは今のところ考慮していません。全部ボディになります。 ※"From “で始まる行を各メールの先頭みなすため、ボディに"From "で始まる行があると予期しない動作をすると思います。

Rubyスクリプトサンプル

  • Thunderbird のメールボックスを想定しています。UNIX MBOX ファイルには拡張子は付与されていないと思います。
  • アカウントが3つ、つまりメールボックス(フォルダ)が3つある環境で動作確認。
  • メールボックスはグループ分けされていてもかまいません。
  • このサンプルではファイル一覧を取得するために “find” というGemsパッケージを使用しています。事前にインストールしてください。
  • このサンプルでは取得したデータの内 “Date"、"Subject"、本文を出力しています。
  • データの量にもよりますが、処理には時間がかかります。適宜スクリプトを修正してください。

●ファイル名:ana_mbox.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
# -*- coding: utf-8 -*-
#---------------------------------------
# MBOXデータを読み込み表示する
#---------------------------------------
#
require 'find'
require 'kconv'

class AnaMbox

  # メールボックス格納フォルダ
  DIR_MBOX = "D:\01_Mail\Thunderbird"

  # [CLASS] 解析
  class Analyze

    # INITIALIZE
    def initialize

      # 読み込み件数初期化
      @cnt = 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|

            flg_body = 0
            header = {}
            body   = []

            # HEADER読み込み
            mail.each do |line|

              # 以下の"kconv"は通常不要ですが
              # 何らかの原因でメールヘッダーに非ASCII文字が存在してしまう場合の
              # 対処のために使用しています。
              line = line.kconv( Kconv::SJIS, Kconv::AUTO )

              # 行最後の改行文字を削除
              line.chop!
              # "From "で始まる行を読み飛ばし
              next if /^From / =~ line
              # HEADERの終了
              if flg_body == 0 && /^$/ =~ line
                flg_body = 1
                next # BODY部分読み込まないなら next を break に変更
              end

              # HEADER
              unless flg_body == 1

                # 属性と値を取得
                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

              # BODY
              else

                body.push(line) #  BODY部分読み込まないなら、この行は削除

              end

            end

            @cnt += 1

            puts "Date: #{header['Date']}"
            header['Subject'] ||= "" # nilなら""を設定
            puts Kconv.tosjis( "Subject: #{header['Subject']}" )
            puts "#" * 40
            body.each do |row|
              puts Kconv.tosjis( row )
            end
            puts "######## COUNT = #{@cnt}"

          end

        end

      rescue => e

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

      end

    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 "====< E N D >===="

  rescue => e

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

  end

end

当方は、上記のスクリプトを更に改良してMySQLに保存することを考えています。。 ※ちなみに、当方の3ドメイン・約7年分のMBOXデータで試したところ約6万件(メルマガ・サーバ管理関連が多数)ありました。 特にボディ部分の “multipart” 関連が難しく、どうしようか思案中。

以上。

Comments