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

Updated:


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

# -*- 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” 関連が難しく、どうしようか思案中。

以上。





 

Sponsored Link

 

Comments