MH形式フォルダをMaildirに変換
必要にかられ,MH形式のフォルダをMaildirに変換するスクリプトを書きました.Rubyで.
MHからMaildirに移行する場合に便利に使えると思います.
Googleなどの検索エンジン経由で見つかるmh2maildirというツールもあるようですが,以下の点がいまいちに感じましたので,自分で作ってしまいました.
- Maildir形式のメールを書き込むためだけにprocmailを使っている
- 未読フラグや返信済みフラグが保存されない(Maildir形式に変更してくれない)
ファイル構成
- mh.rb
- mh2maildir.rbが使うクラス "Mail::Reader::MH", "Mail::Message::MH" を定義
- maildir.rb
- mh2maildir.rbが使うクラス "Mail::Writer::Maildir" を定義
- mh2maildir.rb
- 変換スクリプト本体.
使い方
(1) mh2maildir.rb
Maildirフォルダは自動的に作成されます.指定されたMaildirフォルダが既に存在する場合は動作を停止します(ぽかよけの為)
% ~/bin/mh2maildir.rb Mail Maildir-fromMH
(2) 必要に応じ,変換後のMaildirフォルダを,既存のMaildirと統合
昨日紹介したmddigest, mdmergeを使います.
% ~/bin/mddigest.rb Maildir-fromMH % ~/bin/mddigest.rb Maildir % ~/bin/mdmerge.rb Maildir Maildir-fromMH
実行前にMTAを止めておくこと.
注意点
- 壊れたメールは処理しません
- 「壊れたメール」の定義: ヘッダが一行もないMH形式のファイル
- Courier IMAP専用です
- 他のIMAPサーバ用のMaildirを作るためには,いくつか修正しないといけないかも
- MH形式フォルダに,日本語フォルダ名がある場合はうまく処理できません
- MH形式フォルダ名に "." (ピリオド) を含む場合は,JIS X 0208「.」(いわゆる全角のピリオド)に変換します.
ソース
mh.rb
#!/usr/local/bin/ruby # Mail::Reader::MH class # Mail::Message::MH class # $Id: mh.rb 39 2008-01-08 06:37:09Z genta $ require 'time' require 'find' class Mail class Reader class MH attr_reader :basedir def initialize(basedir) @basedir = basedir end def traverse Find.find(@basedir) do |file| next if File.directory?(file) rpath = file.sub(%r|^#{Regexp.escape(@basedir)}/?|, '') path = rpath.split(File::SEPARATOR) next if path.size < 2 folder = File.join(path[0..-2]) filename = path[-1] next unless filename =~ /^\d+$/ msg = open(folder, filename) next if ! msg.sanity? # skip broken mail yield(msg) end end alias :each :traverse def open(folder, filename) file = File.join(@basedir, folder, filename) message = Mail::Message::MH.new(file) message.folder = folder return message end end # MH end # Reader class Message class MH attr_reader :file, :from, :time, :header, :flag attr_accessor :folder def initialize(file) @header = Hash.new {[]} @flag = {} @file = file open() end def open(file = @file) File.open(file) do |fd| key, key_h, value = '', '', '' fd.each do |line| line.strip! break if line =~ /^$/ # End of the header if /^From (\S+) *(.+)$/ === line then _, @from, time = $~.to_a @time = Time.rfc2822(time) rescue Time.parse(time) next end if not /^([^: \000-\037\177]+): *(.*)/ === line then # line is the tailer of above line @header[key_h][-1] += ' ' + line next end _, key, value = $~.to_a key_h = key.downcase.capitalize @header[key_h] = [] unless @header.key?(key_h) @header[key_h] <<= value end end %w!Seen Replied Forwarded!. select {|x| @header[x].size > 0 }. map {|x| @flag[x] = true } if @time.nil? then retryq = [ lambda { @header['Date'][0] }, lambda { @header['Received'].first.match(/.*;(.*)/).to_a[1] }, lambda { File.mtime(file).to_s } ] begin time = retryq.shift.call @time = Time.rfc2822(time) rescue Time.parse(time) rescue if retryq.empty? then raise "Mail::Message::MH#open: Can't get the date. (file: #{file})" end retry end end end def each fd = File.open(@file) fd.each do |buf| _, rs = /(\r?\n)\Z/.match(buf).to_a next if /^(Seen|Replied|Forwarded): *$/ === buf # cut MH flags if /^From / === buf then if not @from.nil? and not @header.key?('Return-path') yield("Return-Path: <#{@from}>" + rs) end next end yield(buf) break if /^$/ === buf end fd.each do |body| yield(body) end fd.close end def sanity? return false if @header.empty? return true end end # MH end # Message end # Mail
maildir.rb
#!/usr/local/bin/ruby # Mail::Writer::Maildir class # $Id: maildir.rb 38 2008-01-07 18:20:21Z genta $ require 'time' class Mail class Writer class Maildir attr_reader :maildir, :logger @@host = `hostname`.chomp @@pid = $$ def initialize(maildir, logger = nil) @maildir = maildir if ! logger.nil? then @logger = lambda do o = logger dir = nil lambda do |tmpfile| if tmpfile.nil? then o.puts return end if dir.nil? or dir != tmpfile[:dir] then dir = tmpfile[:dir] o.puts o.print "#{dir}: " end o.print "." end end.call() else @logger = lambda {|x| x} # do nothing end create_maildir(@maildir) end def write(reader) reader.traverse do |mh| tmpfile = tmpfile(mh) raise 'Internal error' if test(?e, tmpfile[:file]) @logger.call(tmpfile) create_folder(tmpfile[:dir]) File.open(tmpfile[:file], 'w') do |fd| mh.each {|buf| fd.print buf } end curfile = curfile(tmpfile, mh) File.rename(tmpfile[:file], curfile) File.utime(mh.time, mh.time, curfile) end @logger.call(nil) end private def create_maildir(maildir) if test(?e, maildir) then raise "#{maildir}: Maildir already exists" end system(*%W<maildirmake #{maildir}>) or raise "maildirmake" end def tmpfile(mh) t = Time.now time, usec = t.to_i, t.usec pid = @@pid host = @@host dir = mh2maildir_rpath(mh.folder) file = File.join(@maildir, dir, "tmp", "#{time}.M#{usec}P#{pid}_0.#{host}") return({:time => time, :usec => usec, :pid => pid, :host => host, :dir => dir, :file => file}) end def curfile(t, mh) s = File.stat(t[:file]) info = '' [['Seen', 'S'], ['Replied', 'R']].each do |(label, tag)| info += tag if mh.flag[label] end info = ',' + info if info.size > 0 file = "%d.M%dP%dV%016XI%08X_0.%s,S=%d:2%s" % [t[:time], t[:usec], t[:pid], s.dev, s.ino, t[:host], s.size, info] curfile = File.join(@maildir, t[:dir], 'cur', file) return curfile end def mh2maildir_rpath(rpath_mh) rpath = rpath_mh.gsub('.', '&,w4-') # XXX: replace '.' -> JISX0208 '.' # due to Thunderbird + Courier IMAP # can't handle '.' in folder name. mpath = rpath.split(File::SEPARATOR) if mpath.size == 1 and mpath[0] == 'inbox' then return '' end # Convert MH specific folders to IMAP folders. [['drafts', 'Drafts'], ['trash', 'Trash']].each do |(mh, imap)| mpath[0] = imap if mpath[0] == mh end '.' + mpath.join('.') end def create_folder(dir) return nil if test(?e, File.join(@maildir, dir)) _, folder = /^\.(.*)$/.match(dir).to_a # strip '.' at head of dirname path = folder.split('.') path.size.times do |i| folder = path[0..i].join('.') next if test(?e, File.join(@maildir, '.' + folder)) system(*%W<maildirmake -f #{folder} #{@maildir}>) end end end # Maildir end # Writer end # Mail
mh2maildir.rb
#!/usr/local/bin/ruby # $Id: mh2maildir.rb 37 2008-01-07 16:35:10Z genta $ $LOAD_PATH << File.dirname($0) require 'mh' # Mail::Reader::MH, Mail::Messgae::MH class require 'maildir' # Mail::Writer::Maildir require 'pp' def usage puts "usage: #{File.basename($0)} <MH directory> <Maildir>" puts "Warning: Maildir will be overwritten." end begin $mhdir = ARGV[0] or raise $maildir = ARGV[1] or raise rescue usage() exit(1) end mh = Mail::Reader::MH.new($mhdir) maildir = Mail::Writer::Maildir.new($maildir, $stderr) maildir.write(mh)