RubyでNotesのメールを書き出す

職場のメール環境がNotesだったのですが、社外出向することになり、Notesのない環境で過去メールをどうやって参照するのか問題となりました。Notes標準の機能では、本文は書き出せるのですが、添付ファイルが書き出せません。

そこでRubyの登場です。NotesはOLEサーバになっており、WIN32OLEでぐりぐりいじることが可能なのです。

notes.rb

require 'win32ole'

class Log
  def initialize(s)
    @@logger ||= Proc.new { |x| puts x }
    @@logger.call(s)
  end
  def Log.register_logger(p)
    @@logger = p
  end
end

class Notes
  class Session
    def initialize(server, dbname)
      @server, @dbname = server, dbname
      @session = WIN32OLE.new('Notes.NotesSession') or return nil
      @db = @session.GetDatabase(@server, @dbname)
    end

    # Notes::Documentクラスのイテレータ
    def each
      all = @db.AllDocuments
      doc = all.GetFirstDocument
      while doc
        yield Notes::Document.new(doc)
        doc = all.GetNextDocument(doc)
      end
    end
    include Enumerable

    # 添付ファイルをdirに書き出す
    def extract_files(dir)
      self.each do |doc|
        doc.each_attachment do |obj|
          name = obj.name
          Log.new "\t#{name}"

          # 対象外の拡張子をチェックする
          ext = File.extname(name).downcase
          next if %w(htm html csv pif scr com bat).include?(ext)

          # 書き出しメソッドを呼び出し
          doc.extract_file(name, "#{dir}/#{name}")
        end
      end
    end

    # 添付ファイルをdir/fromに書き出す
    def extract_files_by_sender(dir, body_hash = nil)
      self.each do |doc|
        k = doc.addr(doc.from)
        # 本文はハッシュに入れておく
        if body_hash
          body_hash[k] ||= ""
          body_hash[k] << doc.to_s
        end

        # 出力先の作成
        extract_dir = "#{dir}/#{k}"
        Dir.mkdir(extract_dir) unless File.exist?(extract_dir)

        doc.each_attachment do |obj|
          name = obj.name
          Log.new "\t#{name}"
          ext = File.extname(name).downcase
          next if %w(htm html csv pif scr com bat).include?(ext)
          doc.extract_file(name, "#{extract_dir}/#{name}")
        end
      end
    end
  end

  class Document
    def initialize(doc)
      @doc = doc
    end

    def each_attachment
      a = @doc.Items or return
      a.each do |obj|
        yield Notes::Attachment.new(obj) if obj.Type == 1084 # ATTACHMENT
      end
    end

    def from;    @doc.From[0]; end
    def sendto;  @doc.SendTo[0]; end
    def copyto;  @doc.CopyTo[0]; end
    def subject; @doc.Subject[0]; end
    def posted;  @doc.GetItemValue('PostedDate')[0]; end
    def body;    @doc.GetItemValue('Body'); end

    def addr(s)
      if m = s.match(/CN=(\w+) (\w+)\/O=(\w+)/)
        s = "(#{m[3]}) #{m[2]} #{m[1]}"
      elsif m = s.match(/(\w+) (\w+)\/(\w+)/)
        s = "(#{m[3]}) #{m[2]} #{m[1]}"
      elsif m = s.match(/<([\w.-]+@[\w.-]+)>/)
        s = m[1]
      elsif m = s.match(/([\w.-]+@[\w.-]+)/)
        s = m[1]
      elsif s == ''
        s = '(unknown)'
      end
      s
    end

    def to_s
      ret = ["From: #{from}"]
      ret << "To: #{sendto}"
      ret << "Cc: #{copyto}"
      ret << "Subject: #{subject}"
      ret << "Date: #{posted}"
      ret << ""
      ret << body
      ret << "\n"
      ret.join("\n").gsub("\r", "")
    end

    def extract_file(name, path)
      obj = @doc.GetAttachment(name) or return
      begin
        obj.ExtractFile(path)
      rescue => e
        Log.new "**** #{e.message} ****"
      end
    end
  end

  class Attachment
    def initialize(obj)
      @obj = obj
    end

    def extract_file(path)
      @obj.ExtractFile(path)
    end

    def name
      @obj.Values[0]
    end
  end
end

短いコードなので詳しくは解説しませんが、送信者別にフォルダを作ってそれぞれ本文と添付ファイルを書き出すようになっています。Notes::Document#addrでNotes特有のアドレス表記を若干変換していますがこれはひょっとすると社内仕様かもしれません。

extract-cui.rb

require 'notes'
require 'optparse'

if $0 == __FILE__
  opt_hash = Hash.new
  opt_hash['dir'] = '.'
  ARGV.options do |opt|
    opt.on('-d [dir]') { |v| opt_hash['dir'] = v }
    opt.on('-s') { |v| opt_hash['same_folder'] = v }
    opt.parse!
  end

  dir = File.expand_path(opt_hash['dir'])
  if !File.writable?(dir) || ARGV.size == 0
    Log.new "Usage: ruby #{$0} [-s] [-d dir] file [file...]"
    exit
  end

  body_hash = Hash.new
  ARGV.each do |arg|
    if m = arg.match(/(.*)@(.*)/)
      nsf, svr = m[1, 2]
    else
      nsf, svr = [arg, '']
    end
    if db = Notes::Session.new(svr, nsf)
      Log.new "添付ファイル抽出...(#{arg})"
      if opt_hash['same_folder']
        db.extract_files(dir)
      else
        db.extract_files_by_sender(dir, body_hash)
      end
    end
  end
  Log.new "本文保存..."
  body_hash.each do |k, v|
    Log.new "\t#{k}"
    File.open("#{dir}/#{k}/mail.txt", "w") do |w|
      w.puts v
    end
  end
  Log.new "終了"
end

コマンドライン版のサンプルです。引数はローカル保存した*.nsfファイルです。サーバのメールボックスを直接指定することもできるのですが、危険なのでお勧めしません。^^;VisualuRuby版もあるのですが、省略。

半年以上経つのに何をやっていたかすぐに思い出せましたが、これはRubyだからですねえ。私の場合、PerlVBAだと先月書いたコードでも思い出せなくなります。