MH形式のおさらい(メモ)
MH形式フォルダのおさらい.
- 1メール1ファイル
- メールは「UNIX From行」がつく場合もある
- "From nospam@example.com Tue Mar 14 11:13:50 2000" こんなの
- MTA/MDAがメール配達時にEnvelope Fromの情報を格納するために使う
- Maildir形式に変更するときは,Return-Pathヘッダに変換するのが妥当か?
- メールへのフラグ
- ヘッダを付与することでフラグを表現
- Seenヘッダ: 既読
- Repliedヘッダ: 返信した
- Forwardedヘッダ: Forwardした
- メールのファイル名
- 数字で連番
- フォルダ形式
- シンプルに階層型ディレクトリの構造
Maildir形式に変換するときに考慮しなくてはならない事
Maildirファイル命名規則
MDAによって流儀が異なるらしい.ここではCourier IMAPの流儀に従う.
- ファイル名: cur/time.MusecPpidVdevIino_unique.host,S=cnt:2,info
- time: UNIX time
- usec: ファイルのmtimeのusec
- pid: Process ID.10進数表記.
- dev: デバイス番号.0パディングありの16進数表記で,16桁.(u_long(32ビット)らしい)
- ino: i-node番号.0パディングありの16進数表記で,8桁.(u_int(16ビット)らしい)
- unique: 10進数表記,1桁.
- host: そのホストのFQDN.
- cnt: メッセージのサイズ(バイト数).10進数表記.
- info: フラグ.以下,使いそうなものだけ記述.
- D: Draft
- R: 返信した
- S: 既読
- F: ユーザがフラグを付けた
- フラグが無い場合は,末尾を":2"で終える.
- 例: RS -- 返信した.既読.
- tmpファイルの場合: tmp/time.MusecPpid_unique.host
複数の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)
Domain-0上の物理ディスクをDomain-Uからマウント
Domain-0からアクセス可能な物理ディスクのうちの2つを,Domain-Uから直接mountしたくなったので,いくつか設定の変更を行った.
設定手順(Domain-0)
(1) Domain-Uの設定ファイルに以下を追加
disk = [ 'file:/home/xen/domain-u.img,hda1,w', 'file:/home/xen/domain-u.swp,hda2,w', 'phy:sdb1,hda3,r', # 追加 'phy:sdb1,hda9,r' # 追加 ]
'r'なのは,「phyなディスクをread/writeでマウントするのは現状では危険だぜ」と,Xen3のユーザーズマニュアルに書いてあったから.
どうしてもrwでmountしたいひとは,ここで'w!'とするとよいらしい.
(2) Domain-Uを再起動
% sudo xm shutdown domain-u % sudo xm create domain-u.conf
設定手順(Domain-U)
(1) /etc/fstabに2行追加
/dev/hda3 /freebsd ufs ufstype=ufs2,ro,noauto 0 0 /dev/hda4 /freebsd/usr ufs ufstype=ufs2,ro,noauto 0 0
これでうまくいくはず,だが…….
確認
Domain-Uからmountしてみる
% sudo mount /freebsd mount: unknown filesystem type 'ufs'
失敗.Domain-0からmountする場合は,これでうまくいったのだが.
kernelをリビルドしないといかんのかなぁ.
フィボナッチ数で遊ぶ
日曜プログラマとしては,フィボナッチ数(wikipedia)を生成する関数くらいは呼吸するかのごとく書けるようになりたいと思ったので,書いてみた.Rubyで.
これがまだdoukaku.orgに投稿されていないのは不思議だなぁ…….
イテレイティブに書いた版
はじめに,wikipediaの定義を見ながら書いてみたのがこれ.安直にループを使って書いている.
#!/usr/bin/env ruby def fibonacci(f1, f2) if f1 == 0 and f2 == 0 then return [0, 1] end return [f2, f1 + f2] end maxn = ARGV[0] ? ARGV[0].to_i : 10 arg = [0, 0] p [:n, :fn] for n in (1 .. maxn) arg = fibonacci(*arg) fn = arg[1] p [n, fn] end
リカーシブに書いた版
次に,再起呼び出し(recursive call)を使って書いてみた.
動くことは動くけど,やはりとてつもなく遅い.オーダがO(2^n)だから当然か.
#!/usr/bin/env ruby def fibonacci(n) if n == 1 or n == 2 then return 1 end fibonacci(n - 2) + fibonacci(n - 1) end maxn = ARGV[0] ? ARGV[0].to_i : 10 p [:n, :fn] for n in (1..maxn) p [n, fibonacci(n)] end
キャッシュを使った版
一度計算したものをグローバル変数にキャッシュ.これでだいぶ速くなった.
fibonacci(100)を初めて計算する時に,1〜99がキャッシュに入っていないような状況だと,やはり時間はかかるけど.
#!/usr/bin/env ruby $CACHE = [] def fibonacci_do(n) if n == 1 or n == 2 then return 1 end fibonacci(n - 2) + fibonacci(n - 1) end def fibonacci(n) $CACHE[n] ||= fibonacci_do(n) end maxn = ARGV[0] ? ARGV[0].to_i : 10 p [:n, :fn] for n in (1..maxn) p [n, fibonacci(n)] end
キャッシュ使用版を基に,クラス化した版
キャッシュにグローバル変数を使うのがいまいちなので,クラス変数にした版.
#!/usr/bin/env ruby class Fibonacci @@cache = [] def calc(n) @@cache[n] ||= func(n) end private def func(n) return 1 if n == 1 or n == 2 calc(n - 2) + calc(n - 1) end end maxn = ARGV[0] ? ARGV[0].to_i : 10 fibonacci = Fibonacci.new p [:n, :fn] for n in (1..maxn) p [n, fibonacci.calc(n)] end
末尾再帰版
末尾再帰ふうの書き方で書きなおしてみた.i=1からn==iになるまでi++しながら計算している.
キャッシュはあるものの,全く活用できていないのがいけていない.
#!/usr/bin/env ruby class Fibonacci @@cache = [] def calc(n) @@cache[n] ||= func(n, 1, 0, 0) end private def func(n, i, f1, f2) f1, f2 = 0, 1 if i == 1 or i == 2 if i < n then return func(n, i + 1, f2, f1 + f2) end return f1 + f2 end end maxn = ARGV[0] ? ARGV[0].to_i : 10 fibonacci = Fibonacci.new p [:n, :fn] for n in (1..maxn) p [n, fibonacci.calc(n)] end
末尾再帰 + キャッシュ改良版
末尾再帰版をもとに改良.
最初に,与えられたnに一番近い値をキャッシュから拾ってきて,そこから計算を開始するようにした.
特別扱いをする必要があるn=0, 1, 2の場合を,最初からキャッシュに突っ込んでおくことにより条件分岐を減らしたあたりがチャームポイント.
#!/usr/bin/env ruby class Fibonacci @@cache = [0, 1, 1] def calc(n) @@cache[n] ||= func(n, n0 = find_cache(n-1), *@@cache[n0-2..n0-1]) end private def find_cache(n) return find_cache(n - 1) if @@cache[n].nil? return n end def func(n, i, f1, f2) @@cache[i] = r = f1 + f2 return r if i == n return func(n, i + 1, f2, r) end end maxn = ARGV[0] ? ARGV[0].to_i : 10 fibonacci = Fibonacci.new p [:n, :fn] for n in (1..maxn) p [n, fibonacci.calc(n)] end
まとめ
フィボナッチ数の生成は手あかのついたテーマだと思うが,自分で作ってみるといろいろと工夫するべきポイントが見えてきて面白い.やはり,自分で一度やってみる,ということは重要だと思った.
grepの-fオプションは便利
grep(1)に-f FILEオプションを渡すと,パターンを-eで渡すかわりにFILEから拾ってきてくれる.FILEの中身は,1行に1パターンを書いておく.もちろん-vもちゃんと効く.
% cat pattern foo bar baz % grep -v -f pattern file.to.grep
上記を実行すると,file.to.grepのうち,foo, bar, bazのいずれも含まない行のみ表示する.
私は,溜まったログを読み進めている時など,ぼーっとしてるとgrep -v foo | grep -v bar ... とか,かなり鈍くさいことをしがちであるので,そういう時には-fオプションの存在を思い出すようにしよう.
もし自分がTrackBack/1.02だったら
ステータスコードで不快感を表現したいを読みました.
ここで,「もしも,自分がSPAM Trackbackを行うユーザエージェントを作る立場だったら」という想定をしてみました.
SPAM Trackbackを行うユーザエージェント(以下,botと記述)の目標は,「よりたくさんのWebサイトに,Trackbackを送り付ける」ということになります.このため,なるべくたくさんのWebサイトにHTTPセッションを張り,なるべくたくさんのTrackback pingを投げつける,というのが基本的な動作になります.
このとき,どんな形であれ相手側からレスポンスが返ってくることは,botにとってはとても好都合です.なぜなら,否定的応答が返ってきた場合には,さっさと次のWebサイトに行く,という判断ができるため,単位時間あたりのTrackback ping送信の試行回数を上げることができるからです.
逆にやっかいなのは「Trackback pingを投げたものの,返事がかえってこない」ようなサイトです.このようなサイトにTrackback pingを投げてしまったbotは,TCPセッションのタイムアウトまでの間,動作をとめてじっと待っていなければなりません.その間,次のWebサイトに行くこともできません.
もちろん,bot自身はマルチスレッドまたはマルチプロセスにて動作していることでしょうから,並行して他のWebサイトにも送信を試みるでしょうが,ひとつのホストで処理することのできるTCPセッション数には上限があります.仮に,このような「返事が返ってこない」設定をされたサイトが100や200にも上れば,botにとっては大変都合の悪い環境になるでしょう.
まとめると,botにとってみれば以下のような感じになります.
ということで,Trackback pingを受け取るようなサイトのrootを持っている人で,「"TrackBack/1.02"をなんとかギャフンと言わせてやりたい!」という人は,SPAM Trackbackに対して返事を返すかわりに,ipfw(Linuxだったらipchainsとか?)などのホストベースファイアウォール機構にて動的にdenyルールを追加してしまい,そのTCPセッションについてはACKもFINもRSTももう送らない,という感じにしてしまえばよいんではないかと思います.
問題は,このようにして遮断したTCPセッションは,自サーバ側でも(相手に送ったつもりで,でもipfwで遮断される)HTTPレスポンスのACKパケットを受け取ることができないので,自分もTCPセッションタイムアウトを待つしかない,というあたりかなぁ.これについては要再考.
……あ,今思いついたけど,bot→自分方向はRSTも何も返さず(ipfw的にはdenyアクション),自サーバ→bot方向にはRSTを返送する(ipfw的にはresetアクション)ようにすればいいのかも.httpdがbotに対してHTTPレスポンスを返そうとしたら,ipfwからRSTを食らってTCPセッション開放,という寸法です.