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のファイル名を付与
  • メールファイルの変換
    • UNIX Fromの除去
      • Return-Pathヘッダの追加(既にReturn-Pathヘッダがある場合は追加しない)
    • UNIX Fromからmtimeを取得して,Maildirファイルのmtimeを設定する.
  • メッセージのフラグの対応

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をリストアする時などに使えるかもしれないので,公開しておきます.

概要

  • 指定された2つのMaildirに格納されたメールを,ひとつのMaildirに全てコピーする
  • 重複したメールはコピーしない
  • IMAPフォルダが存在しない場合には,maildirmake(1)コマンドにより作成
  • Courier IMAP

ファイル構成

  • mdd.rb
    • MDDigestクラスを定義.mddigest.rbとmdmerge.rbが使用する.
    • GDBMライブラリを使用.
  • mddigest.rb
  • mdmerge.rb
    • 2つの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オプションの存在を思い出すようにしよう.

その他(どうでもいい話)

昔,私の管理していたSMTPサーバでは,SPAMメールを送りつけてくるようなMTAに対して「550 F*** you spammer!」とか返していたのは,ここだけの秘密です.でも,きっとみんな同じようなことをしていたに違いない.

もし自分が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にとってみれば以下のような感じになります.

  • 200が返ってきた→いい感じ!
  • 403とか返ってきた→まっ,次に行けばいっか.教えてくれてサンキュー
  • TCPセッションタイムアウト待ち→大変マズー

ということで,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アクション)ようにすればいいのかも.httpdbotに対してHTTPレスポンスを返そうとしたら,ipfwからRSTを食らってTCPセッション開放,という寸法です.