横着プログラミング 第10回: scmail: Scheme によるメールフィルタ

最終更新日: 2004-03-11 (公開日: 2003-02-18)

Unix Magazine 誌に 2002年1月号から 2003年2月号にかけて連載し ていた記事の元の原稿です。


プログラミングについての考え方に影響を与えないような言語は
知る価値がない  -- Alan Perlis

*1

Lisp というプログラミング言語がある。プログラミング言語につ いて語られるとき、Lispほど物議を醸す言語はないように思う。 Lispは 1950年代後半に生まれた、高級言語としては最古に近い言 語である。しかし、一部のプログラマの間ではいまだに強い影響力 を持っている。筋金入りの Lisper (Lispプログラマ) である Paul Graham 氏が昨年に発表したエッセイ「普通のやつらの上を行け」 *2 では、Java, Python, Perl, C などの他の言語と比べて Lispこそ が最高の言語であるという主張が展開され、大きな反響を呼んだ。

一方、「伽藍とバザール」 *3 などのオー プンソース関連のエッセイで有名な Eric Raymond 氏は「ハッカー になろう」の中で次のように述べている。 *4

Lisp は、それをモノにしたときのすばらしい悟り体験のために勉
強しましょう。この体験は、その後の人生でよりよいプログラマー
となる手助けとなるはずです。たとえ、実際には Lisp そのものを
あまり使わなくても。

「すばらしい悟り体験」などと言われるとうさんくさい宗教のよう だが、Lisp には確かに何かがあると思う。実際、私も Scheme と いう、Lisp の方言を学んだときに「すばらしい悟り体験」に近い ものを感じた。その頃にまとめたメモには次のような感想が残って いる。

僕はLispを長い間、敬遠してきた。あんな括弧の多い言語、IQの高
い人間じゃないと理解できないに違いない、そもそも実用性がない
んじゃないか、と思っていた。が、やり始めてみると、すぐに病み
つきになった。リスト構造の単純さ、lambda式の考え方、そして、
リストとlambda式の組み合わせによる表現力。限りなく高い自由度。
なんでこんなにすごいんだ?

今になって読み返すとちょっと大げさすぎるかな、という気もする が、当時はずいぶん興奮していたことがわかる。Scheme を学んで からは、他の言語でプログラムを書くときの切れ味もずいぶん変わっ た気がする。今回は私が開発したメールフィルタ scmail*5 を紹介し、あ わせて Scheme にプログラミングの例を紹介する。

メールの自動振り分け

私は、メーリングリストのメールならメーリングリストごとのフォ ルダに、職場からのメールなら職場用のフォルダに、といったよう にメールの振り分けを行っている。大半のメールは自動で振り分け ている。

メールの自動振り分けの方法は、大きく分けて

  1. メールが届いた瞬間 (受信時) に自動振り分け
  2. メールを読み終えた後で受信箱の中を自動振り分け

の 2つがある。1を行うツールとしては procmail*6 や maildrop *7、 slocal*8などが ある。2 は Mew*9 や Wanderlust *10 といったメーラーの機能とし て実装されていることが多い。

私の場合は

  1. 迷惑メールは受信時にゴミ箱行き + あまり読まないメーリングリストは受信時に自動振り分け
  2. その他のメールは受信箱で読み終えた後で自動振り分け

というように、両方の自動振り分けを自作のツール scmail で行っ ている。

scmail とは?

scmail は Scheme で書かれたメールフィルタである。scmail は 次のような特徴を持っている。

  1. Scheme で実装されている (500行程度のシンプルな実装)
  2. 通常の振り分け規則は簡単な S式で書ける
  3. 高度な振り分け規則は Scheme プログラムとして書ける
  4. メールの受信時に自動振り分けができる
  5. メールを読み終えた後で受信箱の中を自動振り分けできる
  6. メールのへッダに含まれる日本語を手軽に扱える

このうち 2 と 3 は Scheme で書かれていることによる大きな特徴 である。scmail で扱う振り分け規則については後ほど詳しく説明 する。

scmail のインストール

scmail を利用するには次のソフトウェアをインストールする。

Gauche

Gauche (ごーしゅ) *11 は Scheme というプログラミング言語の処理系である。Scheme の処理系とし ては他にも Guile*12 などがあるが、Gauche は

といった点で優れている。

Gauche のインストールは次のように行う (原稿執筆時点での最新 版は 0.6.3)。

% gzip -dc Gauche-0.6.3.tar.gz | tar xvf - 
% cd Gauche-0.6.3
% ./configure
% make
% su
Password: (rootのパスワードを入力)
# make install
# make install-doc

Gauche の Webサイトでは Red Hat Linux 7.x 用の RPMパッケージ も用意されているので、Red Hat Linux 7.x を使っている場合は Gauche-eucjp-0.6.3-1.i386.rpm をダウンロードして次のように実 行すればいい。

% su
Password: (rootのパスワードを入力)
# rpm -i Gauche-eucjp-0.6.3-1.i386.rpm (新規インストール)
または
# rpm -U Gauche-eucjp-0.6.3-1.i386.rpm (アップグレード)

scmail

scmail のインストールは次のように行う (原稿執筆時点での最新 版は 0.1)。

% gzip -dc scmail-0.1.tar.gz | tar xvf - 
% cd scmail-0.1
Password: (rootのパスワードを入力)
# make
# make install

scmail をインストールすると scmail-deliver と scmail-refile の 2つのツールが使えるようになる。scmail-deliver はメールを 受信時に自動振り分けするツール、scmail-refile はメールを読み 終えた後で受信箱の中を自動振り分けするツールである。

メールボックスの形式

Unix で使われているメールボックスの形式には主に mbox形式、MH 形式、Maildir形式の 3つがある。現在のところ scmail はこのう ち MH 形式のみに対応している。それぞれのメールボックスの形式 を簡単に説明する。

mbox 形式

mbox 形式は Unix の伝統的なメールボックスの形式である。複数 のメールを単一のファイルに連結して格納し、"From " から始まる 行をメールの区切りとして扱う。/var/spool/mail や /var/mail などのディレクトリに格納されるメールボックスはこの形式である。

mbox 形式のメールボックスでは "From " から始まる行をメールの 区切りとして扱う、という仕様を守るために、メール本文に含まれ る "From " から始まる行は通常 ">From " に書き換えられる。 ">From " から始まる行があった場合はそのまま残るので、本文の 行頭の "From " と ">From "は区別がつかなくなる。

本文の行頭の "From " が ">From " に書き換えられて困るという 場面は滅多にないが、勝手に本文が書き換えられて元に戻らないと いう仕様は気持ちが悪い*13

mbox 形式は仕様が厳密に決まっているわけではなく、本文の "From " 行を ">From " に書き換える代わりに、ヘッダの Content-Length: フィールドに含まれる本文の長さの情報を信じて メールの区切りを識別するシステムも存在する。この辺の事情につ いては初期の Netscape Mail の開発者である Jamie Zawinski 氏 の文書*14 に詳しい。

MH 形式

MH 形式は、MH*15という歴史の ある Unix用のメーラーのメールボックスの形式である。MH 以外の 多くのメーラーでも採用されている。

MH 形式のメールボックスでは、1通のメールを 1つのファイルとし てフォルダに格納する。フォルダとはメールを格納するディレクト リのことである。通常、受信したメールは ~/Mail*16 というメールボックスの下の inbox というフォルダに格納される。格納されたメールには 1, 2, 3, ... のように数字のファイル名がつけられる。

MH 形式では 1メール = 1ファイルで格納されるため、cp や rm コ マンドで簡単に 1通ごとのメールをコピー・削除できる。これは MH形式の大きな利点である。mbox形式では複数のメールが 1つのファ イルに連結されているため、こうはいかない。

Maildir 形式

Maildir 形式は、qmail*17 というメール サーバが導入したメールボックスの形式である。qmail では受信し たメールを各ユーザのホームディレクトリの下のメールボックス (たとえば ~/Maildir) に格納する。このとき、メールを安全に格 納するために tmp, new, cur という 3つのサブディレクトリを使っ た仕組みが用いられている。この安全対策の仕組みを除けば、 MH 形式と同様に 1メール = 1ファイルでメールをフォルダに格納する 形式である。

scmail の設定

scmail を使うには、まず ~/.scmailrc という設定ファイルを準備 する。/usr/local/share/scmail にサンプルの設定ファイルが置か れているので、これを次のようにコピーして使う。

% cp /usr/local/share/scmail/scmailrc.sample ~/.scmailrc

標準の ~/.scmailrc では次のように設定されている。MHのメール ボックスが ~/Mail に存在して、受信用のフォルダ (受信箱) は inbox (~/Mail/inbox) という、 MHの標準的な設定の場合は特に変 更する必要はない。

;; -*- scheme -*-
(
 :mailbox   "~/Mail"         ; メールボックスのディレクトリ
 :inbox     "inbox"          ; 受信用のフォルダ
 :log-file  "~/.scmail-log"  ; ログファイル
 :smtp-host "localhost"      ; メール転送に使う SMTP サーバ
 )

scmail-refile の使い方

scmail-refile はメールを読み終えた後に受信箱の中を自動振り 分けするツールである。受信箱の中のメールを自動振り分けする機 能は大抵のメーラーに備わっているが、私は

という理由で scmail-refile を使っている。

scmail-refile 用の振り分け規則は ~/.scmailrc-refile ファイル に定義する。/usr/local/share/scmail のサンプルを次のようにコ ピーして使う。振り分け規則の書き方については後ほど詳しく説明 する。

% cp /usr/local/share/scmail/scmailrc-refile.sample ~/.scmailrc-refile

scmail-refile はコマンドラインから次のように実行する。

% scmail-refile
refile: inbox/93 -> ml/enkai@coboler/57
refile: inbox/94 -> ml/linux-zaurus/218
refile: inbox/96 -> ml/komatsu-project/26
refile: inbox/98 -> ml/linux-zaurus/219
refile: inbox/99 -> ml/ming/42           

実行結果の最初の行は inbox の 93 番のメールが ml/enkai@coboler というフォルダに 57 番のメールとして振り分 けられた、という意味を示している。~/.scmail-log には同様のレ ポートが時刻付きで記録される。

% tail -5 ~/.scmail-log
2002-09-26T12:49:31: refile: inbox/93 -> ml/enkai@coboler/57  
2002-09-26T12:49:31: refile: inbox/94 -> ml/linux-zaurus/218  
2002-09-26T12:49:31: refile: inbox/96 -> ml/komatsu-project/26
2002-09-26T12:49:31: refile: inbox/98 -> ml/linux-zaurus/219
2002-09-26T12:49:31: refile: inbox/99 -> ml/ming/42

scmail-deliver の使い方

scmail-deliver はメールを受信時に自動振り分けするツールであ る。scmail-deliver 用の振り分け規則は ~/.scmailrc-deliver ファ イルに定義する。/usr/local/share/scmail のサンプルを次のよう にコピーして使う。

% cp /usr/local/share/scmail/scmailrc-deliver.sample ~/.scmailrc-deliver

Sendmail*18 や Postfix*19 などのメールサーバで は ~/.forward ファイルに次のような設定を加えると、メール受信 時に scmail-deliver による自動振り分けが行えるようになる。

| /usr/local/bin/scmail-deliver

POPサーバからのメールの受信に fetchmail*20 を使っ ている場合は ~/.fetchmailrc の mda の設定に /usr/local/bin/scmail-deliver を指定する。

poll pop3.example.org
     protocol apop
     user satoru
     mda "/usr/local/bin/scmail-deliver"
     no mimedecode
     keep                    # メールをサーバに残す設定

メール受信時の振り分けは、うっかり設定を間違えるとメールを失っ てしまう危険性があるので注意が必要である。今のところ scmail-deliver のせいメールを失うというトラブルにはあってい ないが、私は念のために POP でのメール受信を「メールをサーバ に残す」設定にしてscmail-deliver のテストを行っている。

scmail の振り分け規則

scmail の振り分け規則は次の 2つの方法で定義する。

S式とは?

振り分け規則の書き方を説明する前に、S式について簡単に紹介し ておく。S式 (Symbolic Expression) とは Lisp で扱う式を意味す る用語である。S式の定義は

というものであるが、これではさっぱりわからないので、下に例を 挙げる。これらはすべて S式である。

scmail では S式の表記を使って振り分け規則を定義する。しかし、 S式そのものについて深く理解する必要はない。サンプルを参考に して修正を加えればいい。なお、さきほど説明した ~/.scmailrc も S式による表記である。

差出人別の振り分け

メールを差出人別に振り分ける規則を紹介する。差出人別にメール を振り分けるにはヘッダに含まれる

From: foo@example.jp

のようなフィールドの情報を利用すればいい。foo@example.jp か ら届いたメールを from/foo というフォルダ (~/Mail/from/foo) に振り分けるには次のように振り分け規則を定義する。メール受信 時の振り分けの場合は ~/.scmailrc-deliver に、受信箱の振り分 けの場合は~/.scmailrc-refile に振り分け規則を追加する。

(add-filter-rule!
 '(from
   ("foo@example.jp" "from/foo")))

同様に bar@example.jp から届いたメールを from/bar というフォ ルダに振り分けるには次のように規則を追加する。

(add-filter-rule!
 '(from
   ("foo@example.jp" "from/foo")
   ("bar@example.jp" "from/bar")))

add-filter-rule! は振り分け規則を追加する関数である。振り分 け規則は定義が追加された順に適用される。このため、

(add-filter-rule!
 '(from
   ("foo@example.jp" "friends")
   ("foo@example.jp" "from/foo"))))

のように定義すると、foo@example.jp からのメールは friends フォ ルダに振り分けられ、 from/foo フォルダには振り分けられない。 両方のフォルダに振り分けるためには

(add-filter-rule!
 '(from
   ("foo@example.jp" (copy "friends"))
   ("foo@example.jp" "from/foo"))))

のように、先に適用される規則の振り分け先を (copy "...") で囲っ ておけばいい。メールがフォルダにコピーされた後に、以降の振り 分け規則の適用が継続される。

なお、振り分け規則のファイルに括弧の閉じ忘れなどの 文法エラーが見つかった場合は、空の規則ファイルとして扱われ、 振り分け規則の定義は行われない。 ~/.scmail-log に記録される文法エラーのメッセージをみて ファイルを修正する必要がある。

メーリングリストごとの振り分け

メーリングリストごとにメールを振り分ける規則を紹介する。通常、 メーリングリスト経由で送られてくるメールには

X-ML-Name: baz

または

List-Id: <quux.example.jp>

のような特殊なへッダがつけられている。この情報を利用すればメー リングリストごとの自動振り分けが簡単に行える。ヘッダに X-ML-Name: baz というフィールドを持つメーリングリスト baz@example.jp から届いたメールを ml/baz というフォルダ に振 り分けるには次のように振り分け規則を定義する。

(add-filter-rule!
 '(x-ml-name
   ("baz"  "ml/baz")))

同様に、へッダに List-Id: quux.example.jp というフィールドを 持つメーリングリスト quux@example.jp から届いたメールを ml/quux.example.jp というフォルダ に振り分けるには次のように 振り分け規則を追加する。

(add-filter-rule!
 '(x-ml-name
   ("baz"  "ml/baz"))
 '(list-id
   ("quux.example.jp"  "ml/quux.example.jp")))

参加するメーリングリストの数が増えると、メーリングリストごと に規則を定義するのが面倒になる。そこで、次のように正規表現を 使った規則を定義すると、メーリングリストごとに規則を定義する 手間が省ける。

(add-filter-rule!
 '(x-ml-name
   (#/([-.\w]+)/  "ml/\\1"))
 '(list-id
   (#/<([-.\w]+)>/  "ml/\\1")))

この例では、正規表現でパターンマッチを行い、マッチして得られ る文字列を元に、振り分け先のフォルダを決定している。たとえば

X-ML-Name: baz

というメーリングリストの場合は ([-.\w]+) に baz がマッチし、 振り分け先のフォルダは "ml/\\1" の \\1 の部分が baz に展開さ れた"ml/baz" となる。この仕組みは Perl や Ruby で文字列置換 の仕様と同様である。

連載第5回で紹介した、超お手軽なメーリングリストシステム QuickML*21 では使い捨て感覚でどんど んメーリングリストを作っていけるので、いちいち規則を追加して いてはきりがない。上のように一括して処理できる規則を定義して おくと楽である。

メールの転送

メールを転送する規則を紹介する。次の規則は差出人が foo@example.jp か bar@example.jp の場合に、メールを mobile@example.com に転送するというものである。

(add-filter-rule!
 '(from
   (("foo@example.jp" "bar@example.jp")  
    (forward "mobile@example.com"))))

メールを転送する際には ~/.scmailrc の :smtp-host で指定した SMTPサーバ (標準では localhost) が利用される。

迷惑メールの振り分け

迷惑メールを振り分ける規則を紹介する。メールアドレスを Webな どで公開していると、「未承諾広告※無料プレゼント」や「ADULT NEWS」なといった件名の広告メールが頻繁に届く。

こうした迷惑メールへの対策として、2002年7月1日から無差別に送 る国内の広告メールには件名に「未承諾広告※」を表示することが 義務づけられた。Subject: フィールドに「未承諾広告※」が含ま れるメールを junk フォルダに振り分ける規則を次に示す。

(add-filter-rule!
 '(subject
   ("未承諾広告※" "junk")))

通常、日本語の件名は RFC 2047 *22 の MIME の標準に従って特殊な文字列に符号化されているが、 scmail では自動的に復号化が行われるため、「未承諾広告※」と いう文字列で普通にパターンマッチできる。

ところで、悪質な広告送信業者は、フィルタによって振り分けられ るのを防ぐために「未承認広告※」にしてみたり「未承諾広告*」 にしてみたりと姑息な手を使ってくるようである。あほらしい気も するが、これらに対応するには正規表現のパターンを使う手がある。

(add-filter-rule!
 '(subject
   (#/[未末]承[認諾]広告[**※]/ "junk")))

最近では中国や韓国からの迷惑メールも増えてきている。中国語と 韓国語は一切読めないという人は次の振り分け規則を追加するとい いかもしれない。この規則では Content-Type: フィールドの文字 コードを見て、中国語や韓国語の文字コードならばjunk フォルダ に振り分ける。

(add-filter-rule!
 '(content-type
   (#/gb2312|euc-kr|big5|gbk|ks_c_5601/i "junk")))

最後に、Schemeプログラムで書いた高度な振り分け規則を紹介する。 次の振り分け規則は、

にメールを junk フォルダに振り分ける。

# 2004-03-11: scmail 1.0以上のスタイルに修正しました
(define junk-words
  '("money" "adult" "sex" "viagra"))

(add-filter-rule!
 (lambda (mail)
   (and (not (any (cut mail 'subject <>) junk-words))
        (mail 'body "<HTML>")
        (refile mail "spam"))))

最初の (define junk-words ...) で迷惑メール風の単語のリスト を定義して、(lambda (mail) ... で始まる無名のコールバッ ク関数で振り分けを行う。より複雑な振り分け規則も Scheme プロ グラムとして自由に書ける。procmail (後述する変態的な文法)や maildrop (C言語風の文法) といったメールフィルタでもプログラ ミング言語風に振り分け規則を書けるが、Scheme ほどの自由度は ない。

迷惑メールを自動振り分けするツールとしては、 spamassasin*23 が人気が高い。 spamassasin は遺伝的アルゴリズムで算出したスコアを規則ごとに 割り振って、スコアの合計によって迷惑メールの判定を行う。

振り分けしない派

メールの検索が十分に速ければ振り分けなどする必要はない、とい う考え方がある。この考え方では、すべてのメールを振り分けずに 一か所に溜めておく。そして、fooメーリングリストのメールをま とめて読みたくなったときに X-ML-Name: foo で検索をかけて仮想 的なフォルダを作成する、というように動的に振り分けを行う。

この「振り分けしない派」を実現するために、メールをデータベー スで一括管理するメーラーや、全文検索システムと連携できるメー ラーが存在する。しかし、どうも流行っていないところをみると、 メールを小まめに振り分けておく方が、必要なときに「えいやっ!」 と検索をかけて振り分けるより、人間の性質に適しているのかもし れない。が、この点についてはまだよくわかっていない。メーリン グリストのように振り分け規則が明白である場合は、必要なときに 検索をかけて振り分ける方法でも問題なさそうだが、「友人」や 「研究」のように機械的な振り分けが難しい分類に対しては、日頃 から手動でこまめに振り分けていくしかないのかもしれない。

Scheme プログラミングの例

ここでは Scheme プログラミングの例を紹介する。 Scheme を触る にはコマンドラインから gosh を実行する。対話モードでインタプ リタが走り、プログラムを 1行入力すると結果がすぐに返ってくる。

% gosh
gosh> (list 1 2 3 4 5 6 7 8 9 10)  
(1 2 3 4 5 6 7 8 9 10)
gosh> (+ 1 2 3 4 5 6 7 8 9 10)
55
gosh> (map (lambda (x) (* x 2)) (list 1 2 3 4 5 6 7 8 9 10))
(2 4 6 8 10 12 14 16 18 20)
gosh> (apply + (map (lambda (x) (* x 2)) (list 1 2 3 4 5 6 7 8 9 10)))
110

この例では上から順に

という処理を行っている。この例から Scheme プログラミングには 次の 3つの特徴があることがわかる。

もっとも大きな特徴は最後の見た目の問題である。C言語を知って いれば、なんとなく Perl のプログラムを読めるし、Perlを知って いれば、なんとなく Ruby のプログラムを読める。しかし、Scheme の場合はそうはいかない。私が Scheme を含め Lisp 全般を敬遠し ていた大きな理由はこの見た目の独特さによるところが大きい。

しかし、見た目の問題は一度慣れてしまえば後は気にならない。括 弧が多いのは、プログラムとデータを S式という同じ構造で扱うと いう Lisp の特徴を反映したものである。Scheme プログラミング の参考書としては『計算機プログラムの構造と解釈』*24 が定評がある。これは Scheme そのものの参考書ではなく、 Scheme を使ったプログラミングの教科書である。

Scheme の言語仕様は R5RS *25 とい う文書に、 50ページと大変コンパクトにまとめられている。 Scheme そのものは最低限の機能しか持たないため、実用のツール を作るには多くのライブラリが必要になる。その点、 Gauche は標 準で便利なライブラリを豊富に備えている。

次の関数は Gauche の正規表現ライブラリを利用して、メールのヘッ ダの MIME 符合を復号化するものである。scmail のソースコード から抜粋した。

"あ\n abc"

"あabc"

のような符号化された文字列を "あabc" に復号化する。

(define (decode-field str to-code)
  (with-error-handler 
   (lambda (e) str)
   (lambda ()
     (regexp-replace-all #/=\?([^?]+)\?([BQ])\?([^?]+)\?=\s*/ 
                         str
                         (lambda (m)
                           (let* ((charcode (rxmatch-substring m 1))
                                  (encoding (rxmatch-substring m 2))
                                  (message  (rxmatch-substring m 3))
                                  (decode (if (equal? encoding "B")
                                              base64-decode-string
                                              quoted-printable-decode-string)))
                             (ces-convert (decode message)
                                          charcode
                                          to-code)))))))

次の関数は、Gauche のネットワークライブラリを利用して、メー ルを送信するものである。こちらも、scmail のソースコードから 抜粋した。

(define (send-mail host port iport mail-from recipients)
  (with-error-handler
   (lambda (e) (errorf "send-mail failed: ~a" (slot-ref e 'message)))
   (lambda ()
     (call-with-client-socket
      (make-client-socket 'inet host port)
      (lambda (in out)
        (let ((send-command 
               (lambda (command code)
                 (when command (format out "~a\r\n" command))
                 (let* ((line (read-line in))
                        (return-code (string->number (substring line 0 3))))
                   (if (eq? return-code code)
                       line
                       (errorf "smtp-error: ~a => ~a" command line))))))
          (send-command #f 220)
          (send-command (format "HELO ~a" (sys-gethostname)) 250)
          (send-command (format "MAIL FROM: <~a>" mail-from) 250)
          (for-each (lambda (rcpt)
                      (send-command (format "RCPT TO: <~a>" rcpt) 250))
                    (if (string? recipients) (list recipients) recipients))
          (send-command "DATA" 354)
          (port-for-each (lambda (line)
                           (format out "~a\r\n"
                                   (regexp-replace #/^\./ line "..")))
                         (lambda () (read-line iport)))
          (send-command "." 250)
          (send-command "QUIT" 221)))))))

localhost のポート25番で動いている SMTPサーバを利用して "foo@example.org" から "bar@example.com" へメールを送信する には次のように関数を呼び出す。メールは標準入力から読み込むも のとする。

(send-mail "localhost" 25 (current-input-port)
           "foo@example.org" "bar@example.com")

このように、Gauche を使えば、Perl や Ruby と同様の手軽さで Scheme を使って実用的なプログラムを書ける。

Emacs で楽々 S式

括弧が多くて煩わしい、という理由で S式は毛嫌いされることが多 い。括弧については、竹内郁雄氏の次のような言葉がある*26

弟子が尋ねた。「先生、私は先生がカッコをまるで魔術師のように
扱っているのを常々敬服しています。どうすれば先生のようになれ 
るのでしょうか?」
師「えっ? カッコ? あ、そうか。そんなものもあったな。いやあ、
すっかり忘れておったわ」

この境地になかなか達するのは難しいとしても、Emacs を使えば、 S式を比較的容易に操作することができる。私の場合は Emacs を使っ ているぶんには括弧の存在はさほど気にならなくなってきた。

対応する括弧を光らせる

~/.emacs に次の行を追加すると、対応する括弧を光らせることが できる。

(show-paren-mode)

show-paren-mode を設定すると、カーソルが開き括弧の上に乗った ときと、閉じ括弧の後ろにきたときに、対応する括弧が自動的に光 る。下の画面では閉じ括弧の後ろの位置にカーソルを置いて、対応 する括弧を光らせている。

対応する括弧を光らせる
対応する括弧を光らせる

S式を扱うキー操作

Emacs には S式を扱うための便利なキー操作が数多く用意されてい る。これらすべてを覚える必要はないが、 C-M-k*27C-M-SPC *28 は覚えておくと特に便利である。開き括弧の上にカーソルを乗せて C-M-k を叩けば対応する括弧の内側をすべて削除できる。

S式を扱うキー操作を以下にまとめておく。

C-M-k           S式を削除する
C-M-SPC         S式をマークする
C-M-f           ひとつ先のS式にカーソルを進める
C-M-b           ひとつ前のS式にカーソルを戻す
C-M-t           前後のS式の順序を入れ換える
C-M-q           S式をインデントする
C-M-backspace   ひとつ前のS式を削除する
C-M-n           ひとつ先のリストにカーソルを進める
C-M-p           ひとつ前のリストにカーソルを戻す
C-M-d           ひとつ内側のリストにカーソルを進める
C-M-u           ひとつ外側のリストにカーソルを戻す

lisp-mode や scheme-mode では TAB キーで適切に S式をイ ンデントできる。scmail の設定ファイルでは最初の行に

;; -*- scheme -*-

というコメントを入れて、Emacs で設定ファイルを開くだけで scheme-mode で編集できるように細工している。

括弧の色を薄くする

画面上が括弧だらけという見た目をやわらげるために、私はEmacs で Scheme のプログラムを書くときは括弧の色を薄くしている。 Scheme でしばらく試して気分がよかったため、C言語でも同様に括 弧の色を薄くしてみた。

(show-paren-mode)
(show-paren-mode)
(show-paren-mode)
(show-paren-mode)

括弧の色を薄くするには 次の設定を ~/.emacs に加えればいい。

  ;; 小括弧 () の色を定義
  (defvar paren-face 'paren-face)
  (make-face 'paren-face)
  (set-face-foreground 'paren-face "#88aaff")
  
  ;; 中括弧 {} の色を定義
  (defvar brace-face 'brace-face)
  (make-face 'brace-face)
  (set-face-foreground 'brace-face "#ffaa88")
  
  ;; 大括弧 [] の色を定義
  (defvar bracket-face 'bracket-face)
  (make-face 'bracket-face)
  (set-face-foreground 'bracket-face "#aaaa00")
  
  ;; lisp-mode の色設定に追加
  (setq lisp-font-lock-keywords-2
        (append '(("(\\|)" . paren-face))
                lisp-font-lock-keywords-2))
  
  ;; scheme-mode の色設定に追加
  (add-hook 'scheme-mode-hook
            '(lambda ()
               (setq scheme-font-lock-keywords-2
                     (append '(("(\\|)" . paren-face))
                             scheme-font-lock-keywords-2))))
  
  ;; c-mode の色設定に追加
  (setq c-font-lock-keywords-3
        (append '(("(\\|)" . paren-face))
                '(("{\\|}" . brace-face))
                '(("\\[\\|\\]" . bracket-face))
                c-font-lock-keywords-3))

おわりに

今回は、私が開発したメールフィルタである scmail を紹介し、あ わせて Scheme プログラミングの例を紹介した。scmail はこだわ りを持って Scheme で実装したが、プログラムの書きやすさという 点では Ruby で書いた方が楽だったような気もする。とはいうもの の、 Scheme プログラミングは他の言語と違った魅力を持っている のは確かである。本記事を読んで Scheme プログラミングに興味を 持ってもらえるとうれしい。

なお、本連載で作成したプログラムのソースコードは筆者のページ *29 から入手可能である。

余談: procmail恐怖症

メールを受信時に自動振り分けするツールとしては procmail が広 く使われている。しかし、私は procmail を一度は使おうと試みた ものの、その変態的な文法に拒絶反応を示して、procmail と聞い ただけで逃げ出したくなる病気 (procmail恐怖症) にかかってしまっ た。

procmail 恐怖症の人間は私だけではなく、Simon Cozens 氏は、 Perlによるメールフィルタを紹介した記事*30 の 冒頭で「事実から目をそらすのはやめよう。procmailは最悪だ」と 主張している。

世の中には、使い方が難しいツールほど Web 上の情報が充実する、 という法則があるらしく、procmail も、多くの解説が Web 上で公 開されている。その中でも特に充実している藤田充典氏のサイト *31 を見ると、 次のような振り分け規則 (procmail の用語ではレシピと呼ぶ) が 載っている。

# Subject: に「未承諾広告※」が含まれるメールを 
# $MAILDIR/trash へ振り分けるレシピ
:0
* ^Subject:.*iso-2022-jp
* ^Subject:.*\/.*
* ? echo "$MATCH" | nkf -meZ1 | sed 's/[[:space:]]//g' | egrep '未承諾広告※'
$MAILDIR/trash/.

ここでは procmail 恐怖症を押さえて、この振り分け規則の解読を試みてみ たい。

1行目 :0

そもそも私が procmail 恐怖症にかかったのは :0 という謎の行が 原因である。どうも規則の始まりを示す印のようであるが、0 とい う数字の意味は何なのだろうか。 man procmail で procmail のマ ニュアルを読むと、何の前置きもなく :0 が登場して説明が見当た らない。あれこれ調べた結果、 man procmailrc の一番下に次のよ うな記述が見つかった。

その昔、規則の開始を示す :0 の部分は、条件の数 n に応じ
て `:n' と書き換える必要があった。

つまり、今となっては 0 に意味はなく、ただの過去の遺物として 残っているだけである。これには頭がくらくらした。

2行目 * ^Subject:.*iso-2022-jp

* から始まる行には条件が定義される。この例では Subject: にiso-2022-jp が含まれる、という条件を正規表現で定 義している。

3行目 * ^Subject:.*\/.*

この行も条件を定義している。が、正規表現に含まれる見慣れない 表記 \/ は何だろうか? これは \/ 以降でマッチする文字列を $MATCH という変数に格納するためのおまじないである。Perl や Ruby などの言語では /^Subject:(.*)/ でマッチさせて、括弧の中 でマッチした文字列を $1 に格納するという仕様が一般的だが、 procmail では \/ という独特の表記を採用している *32

4行目 * ? echo "$MATCH" | nkf -meZ1 | sed 's/[ \r\n\t\v]//g' | egrep '未承諾広告※'

* に続く ? は外部のプログラムを実行してその終了ステー タスをみる、という条件を定義するためのコマンドである。この行 では、$MATCH に格納された文字列を nkf *33で変換して、 sed で余計な空白を取り除いた後に、egrep で「未承諾広告※」を マッチさせて、見つかったら真、となる条件を定義している。

なぜこのような大掛かりなことをしているかというと、日本語の メールの Subject: は MIME の標準に従って

Subject: 「未承諾広告※横着連載よろしく」
                 ↓
Subject: "未承諾広告※横着連載よろしく"

のように符号化されているため、nkf でこの符号化を解く必要があ るからである。nkfは日本語の文字コードを変換する機能とともに、 MIME のヘッダを復号化する機能も備えている。

5行目 $MAILDIR/trash/.

以上のすべての条件がマッチしたら、メールを $MAILDIR/trash/. に格納する。このとき、

という動作が行われる。

Subject: に「未承諾広告※」が含まれるメールを振り分けたいだ けなのに、複雑な規則を書いて、外部プログラムをたくさん呼び出 すのは何か間違っている気がする。

procmail恐怖症の原因は :0 という謎の行だけではなく、あらゆる コマンドやフラグが * ! ? $ : c f B などの 1文字の記号で表記 されていることにもよる *34。Unix の昔ながらのツールの設定ファイルではこ ういった 1 文字表記の暗号的なコマンド名や変数名が散見される が、私はこれを Unix文化の悪い面だと思っている。

どうも計算機に習熟した人間は、使いにくかったり設定が難しかっ たりするツールのことを「奥が深い」と取り違えて、多大な労力を 払って受け入れてしまう傾向があるように思う。複雑怪奇な使いに くいツールが長く使われ続けるという現象は、この傾向から説明で きるような気がする。

参考文献


Satoru Takabayashi

*1Alan Perlis, "Epigrams on Programming", ACM SIGPLAN Notices, Vol. 17, pp. 7-13, 1982. <http://www-pu.informatik.uni-tuebingen.de/users/klaeren/epigrams.html>
*2<http://www.shiro.dreamhost.com/scheme/trans/beating-the-averages-j.html>
*3<http://cruel.org/freeware/cathedral.html>
*4<http://cruel.org/freeware/hacker.html>の山形浩生氏の訳か ら引用。
*5<http://0xcc.net/scmail/>
*6<http://www.procmail.org/>
*7<http://www.flounder.net/~mrsam/maildrop/>
*8MHに付属するツール。MHについては後述する。
*9<http://mew.org/>
*10<http://www.gohome.org/wl/>
*11<http://shiro.dreamhost.com/scheme/gauche/>
*12<http://www.gnu.org/software/guile/>
*13この問題を解決するために、">From " を ">>From " に書き換える mboxrd 形式がある。 <http://www.jp.qmail.org/q103/jman5/mbox.html>
*14<http://www.jwz.org/doc/content-length.html>
*15<http://www.ics.uci.edu/~mh/>
*16~/Mail は自 分のホームディレクトリの下の Mail ディレクトリである。私の場 合は /home/satoru/Mail。
*17<http://qmail.org/>
*18<http://www.sendmail.org/>
*19<http://www.postfix.org/>
*20<http://www.tuxedo.org/~esr/fetchmail/>
*21<http://QuickML.com/>
*22Request For Comments <http://www.rfc-editor.org/> などから入手できるインターネッ ト関連の文書。詳しくは本紙連載の「RFCダイジェスト」を参照
*23<http://spamassassin.org/>
*24末に参考 文献
*25 Revised^5 Report on the Algorithmic Language Scheme <http://www.schemers.org/Documents/Standards/R5RS/>
*26末に 参考文献
*27C-M-k は Ctrl キーと Alt キーを押しなが ら k を押すという操作
*28C-M-SPC は Ctrl キーと Alt キーを押しながらスペースキーを押すという操作
*29<http://0xcc.net/unimag/>
*30末に参考文献
*31<http://www.jaist.ac.jp/~fjt/procmail.html>
*32このため List-Id: <foo.example.org> というフィールドから <...> の中を マッチさせるには「* ^List-Id: <\/.*>」に続けて、末尾の > を 削るために「* MATCH ?? ^\/[^>]+」という規則を書く必要がある。 ううむ…。
*33ネットワーク漢字フィ ルター<http://www.ie.u-ryukyu.ac.jp/~kono/nkf/>
*34ちなみに、procmail のソースコード は見た目の独特さという点で procmail の設定ファイルと共通点を 持っている。