複数のMaildirをひとつにマージするスクリプト
複数のMaildirに格納されたメールを,ひとつのMaildirに統合するスクリプトを書きました.Rubyで.
バックアップからMaildirをリストアする時などに使えるかもしれないので,公開しておきます.
概要
ファイル構成
使い方
マージ元とマージ先のMaildirがそれぞれ以下のようになっているものとする.
- マージ先
- /home/user/Maildir
- マージ元
- /home/user/Maildir-backup
(1) MTAとIMAPサーバを停止する
(2) 2つのMaildirに対し,mddigest.rbを実行
この操作により,それぞれのMaildirの直下に "mddigest" ファイルが作成される.
% ~/bin/mddigest.rb /home/user/Maildir % ~/bin/mddigest.rb /home/user/Maildir-backup
(3) マージ先のMaildirをバックアップ
マージ先のMaildirは上書きされるため,念のためにバックアップする.
Maildirでは,ファイルのmtimeをメール到着時間として利用するため,cp -pオプションを付けておく.
% cp -Rp /home/user/Maildir /home/user/Maildir.bak.mdmerge
(4) mdmerge.rb実行
% ~/bin/mdmerge.rb /home/user/Maildir /home/user/Maildir-backup
これにより,Maildir-backupに存在するメール(およびIMAPフォルダ)はすべてMaildirにコピーされる.
(5) IMAPサーバを起動し,メーラから確認
(6) MTAを起動
ソース
mdd.rb
#!/usr/local/bin/ruby # $Id: mdd.rb 29 2008-01-05 08:25:26Z genta $ require 'gdbm' require 'digest/sha1' require 'pp' # Maildir digest file class class MDDigest attr_reader :path, :basedir, :db def initialize(maildir) @basedir = maildir.dup @path = File.join(@basedir, 'mddigest') open() end def open; @db = GDBM.open(@path); end def close; @db.reorganize.close; end def clear; @db.clear; end def add(path) apath = abspath(path) md = hash(apath).hexdigest size = File.size(apath) key = md + '|' + size.to_s if @db.key?(key) then merge(key, path) return end @db[key] = path end def has_file?(key, file) return false if @db[key].nil? @db[key].split('\0').each do |src| return true if src == file end return false end def each(&block) @db.each(&block) end def hash(file) md = Digest::SHA1.new File.open(file, 'r') do |fd| buf = '' while fd.read(256, buf) md << buf end end return md end def abspath(file) File.expand_path(file, @basedir) end def merge(key, dest) dfolder = folder(abspath(dest)) list = @db[key].split('\0') list.map! do |src| return if src == dest if folder(src) == dfolder then return if File.mtime(abspath(dest)) < File.mtime(abspath(src)) next end src end.compact! list += [dest] raise "Idential" if list.join('\0') == @db[key] @db[key] = list.join('\0') end def folder(path) f, = path.split('/') return '' unless f =~ /^\./ return f end end
mddigest.rb
#!/usr/local/bin/ruby # mddigest.rb -- Make message digest file of specified Maildir. # $Id: mddigest.rb 35 2008-01-06 13:57:06Z genta $ $LOAD_PATH << File.dirname($0) require 'mdd' # MDDigest class require 'find' require 'pp' def usage puts <<-_EOD_ #{File.basename($0)} [Maildir] [digest_filename] _EOD_ end def getargs $home = ENV['HOME'] or raise "HOME environment undefined" $maildir = ARGV[0] || $home + '/Maildir' end getargs() md = MDDigest.new($maildir) md.clear Find.find($maildir) do |fpath| rpath = fpath.sub(%r|^#{Regexp.escape($maildir)}/?|, '') folder, = path = rpath.split(%r|/|) if test(?d, fpath) then case path.size when 0; next when 1, 2 next if path[-1] =~ /^(cur|new|tmp)$/ Find.prune if path[-1] =~ /^courierimap(keywords|hieracl)$/ next if folder =~ /^\.[^\/]+$/ else; raise "#{rpath}: Not impremented this case" end raise "path: #{path.inspect}: Internal error" end file = path[-1] next if file =~ /^courierimap(subscribed|uiddb)$/ next if file =~ /^procmail\.log$|^mddigest(\.\w+)?$/ raise "#{file}: Internal error (path.size = #{path.size})" if path.size < 2 # Now, folder: folder's name, file: filename of entire mail. # and, (2 <= path.size and path.size <=3) == true. raise "#{path.inspect}: Internal error" if (path.size < 2 or 3 < path.size) next if file =~ /^(courierimapacl|maildirfolder)$/ md.add(rpath) end md.close __END__
mdmerge.rb
#!/usr/local/bin/ruby # mdmerge.rb -- Merge two maildir into one. # $Id: mdmerge.rb 36 2008-01-06 13:58:51Z genta $ $LOAD_PATH << File.dirname($0) require 'mdd' # MDDigest class require 'pp' class CMD attr_reader :fileq, :dirq, :skipdir, :basedir, :fromdir def initialize(basedir, fromdir) @fileq, @dirq = [], [] @skipdir = [] @basedir = basedir.dup @fromdir = fromdir.dup end def add(file) mkdir(file) cpfile(file) end def cpfile(rpath) return if @fileq.include?(rpath) @fileq << rpath end def mkdir(rpath) rdir = File.dirname(rpath) path = rdir.split(File::SEPARATOR) path.pop if path[-1] =~ /^(cur|new|tmp)$/ rdir = File.join(path) adir = abspath(rdir) return if @dirq.include?(rdir) or @skipdir.include?(rdir) if test(?d, adir) then puts "mkdir: already exists: #{adir}" @skipdir << rdir return end @dirq << rdir end def commit_check @dirq.each do |dir| puts "mkdir: #{dir}" end @fileq.each do |file| puts "cp: #{file}" end end def commit do_mkdir do_cpfile end def do_mkdir @dirq.each do |rpath| folder = rpath.split(File::SEPARATOR)[0].sub(/^\./, '') #puts "maildirmake: #{folder} (from: #{rpath}, basedir: #{@basedir})" system('maildirmake', '-f', folder, @basedir) or raise "maildirmake: #{$?}" end end def do_cpfile @fileq.each do |rpath| tofile = File.expand_path(rpath, @basedir) frfile = File.expand_path(rpath, @fromdir) system('cp', '-p', frfile, tofile) or raise "cp: #{$?}" end end def abspath(file) File.expand_path(file, @basedir) end end def usage puts <<-_EOD_ #{File.basename($0)} [base-maildir] [merge-from-maildir] Note: base-maildir will be overwritten. Please backup first. _EOD_ end def getargs $home = ENV['HOME'] or raise "HOME environment undefined" if ARGV.size != 2 then usage() exit end $src, $dest = ARGV[0..1] end getargs() smd = MDDigest.new($src) dmd = MDDigest.new($dest) cmd = CMD.new(smd.basedir, dmd.basedir) dmd.each do |key, file| files = file.split('\0') files.each do |file| cmd.add(file) unless smd.has_file?(key, file) end end cmd.commit smd.close dmd.close __END__
mdview.rb
おまけ.mddigest.rbで作成したハッシュ値格納ファイルの中身を閲覧する.
#!/usr/local/bin/ruby # mdview.rb -- view gdbm file # $Id: mdview.rb 23 2008-01-04 19:40:43Z genta $ require 'gdbm' require 'pp' def view(file) unless $flag_multiply then pp GDBM.open(file).map return end GDBM.open(file).each do |key, value| values = value.split('\0') next if values.size <= 1 pp [key, *values] end end if ARGV[0] =~ /^-m/ then $flag_multiply = true; ARGV.shift end file = ARGV[0] or raise "need arg.\nUsage: #{File.basename($0)} gdbm-file\n" raise "#{file}: Not exists or not a file" unless test(?f, file) view(file)