Logo address

Plan9 sendmail

1998/08/23

sendmailはメールの仕分けを行う。このプログラムには外部からのメールも内部からのメールも送り込まれる。sendmail は宛先アドレスに応じてメールを適切に仕分けする任務を負っている。
sendmail によってメールはユーザのメールボックスに届けられるか、あるいは、インターネットのどこかのホストへ配送される。

sendmail は仕分けを変換規則に基づいて行う。この変換規則は /mail/lib/rewrite に記述されている。

以下に sendmail が処理するデータとメールの仕分け先を簡単に図示する。


メールアドレス

Plan9 ではメールアドレスの内部処理をインターネットで普通に使用されている形式(インターネット形式)ではなく、UNIX で旧くから使用されていたアドレス形式( UUCP 形式)でおこなう。即ち、
arisawa@ar.aichi-u.ac.jp
ではなく
ar.aichi-u.ac.jp!arisawa
と表現する。 sendmail はもっと複雑なアドレスを処理する必要がある。
A,b,C をホストアドレス(例えは ar.aichi-u.ac.jp の様な)とせよ。以下の3つは同じ意味であるが sendmail はどの形式も扱う必要がある。 ここに alice はホストC のユーザである。このアドレスはメールがホストA、ホストb、ホストC を経由して alice に届けられる事を要請している。この形式のアドレスはルートアドレスと呼ばれている。 UUCP形式では と書かれるだろう。

ホスト名 local はメールを受け取ったホストを意味している。このホスト名が ar であれば
local!alice

ar!alice
と等価である。


正規表現

UNIX の sendmail.cf はゴルディオスの結び目に例えられるほど難解な代物である(文献[1])。 sendmail.cf に相当する Plan9 のファイルは/mail/lib/rewrite を中心とした幾つかのファイルである。
UNIX と異なり Plan9 では正規表現が上手に使われている。管理者レベルの力量があれば、設定を好みに合わせて容易に変更できる。

正規表現を使用するメリットは、強力である事、そして(正規表現を知っている者にとっては)新たな知識を必要としない事である。

以下に正規表現に馴染みの無い読者を対象に必要な限りに於て正規表現を解説する。sendmail が扱う正規表現には sendmail 独自の拡張が含まれているので正規表現を知っている読者も目を通してほしい。

正規表現は文字列の集合を表現する。

メールアドレスに現われる文字列は ASCII の印字文字(`!'- `~')の集合である。従って sendmail が扱う正規表現ではこの文字集合を基礎にする。この文字集合を基礎文字集合と呼ぶ。正規表現の記号 `.' は基礎文字集合を表す。

文字集合は列挙する形式でも表現される。そのためには記号 `[' と `]' が使用される。[!@] は2つの文字 `!' と `@' から構成される文字集合である。[ ] の中の先頭に使用される記号 `^' は否定を表す。[^!@] は2つの文字 `!' と `@' を含まない文字集合、即ち、`.' から [!@] を差し引いた文字の集合である。

メールアドレスは英字の大文字と小文字を区別しない。そこで sendmail が扱う正規表現も同様にこの区別を行わない。 従って正規表現 alice は "alice", "Alice", "aLice", ... 等の 32 個の文字列の集合を意味する。

和集合は記号 `|' で表現される。正規表現 alice|bob|carol は3つの正規表現 alice, bib, carol の和集合であり、合計 32 x 8 x 32 個 の文字列を要素とする集合である。

文字集合に続く記号 `*' はその文字集合に含まれる文字が 0 個以上続く事を意味している。記号 `+' は `*' とよく似ているが、文字が 1 個以上続く事を意味している。 従って[^!@]+ は `!' と `@' を含まない長さが 1 以上の文字列の集合である。他方 [^!@]* には長さが 0 の文字列が含まれる。

文字列sが文字列集合Sに含まれる時にマッチすると言う。 文字列集合S が正規表現rによって表現されている時、文字列 s と正規表現 r はマッチすると言う。
正規表現の Postmaster には文字列 pOsTmAsTer も postmaster もマッチする。また 正規表現 .* には任意の文字列がマッチする。

文字列の置換を扱う場合には記号 `(' と `)' が役に立つ。正規表現に含まれる ( ) で囲まれた範囲が置換の際に指定できるのである。 文字列 local!alice は 正規表現 local!(.*)にマッチする。 この場合 (.*) の部分は alice とマッチしている。

記号 ( ) は複数使用できる。 文字列 alice@ar.aichi-u.ac.jp は 正規表現 ([^@]+)@([^@]+) にマッチする。 この場合最初の([^@]+)は alice とマッチしており、第二の([^@]+)は ar.aichi-u.ac.jp とマッチしている。

正しいアドレスではないが文字列 alice@@@ar.aichi-u.ac.jp は正規表現 (.+)@(.+) にどのようにマッチしているのだろうか?
この場合には最初の (.+) は alice@@ にマッチし、第二の (.+) は ar.aichi-u.ac.jp にマッチする。ルールは次の様になっている。
最初の ( ) は残りの文字列のマッチングが妨げられない範囲で最大の文字長を選択する。次の ( ) もまた同様な原則でマッチする範囲が定められていく。

正規表現の中に ( ) が使用されている時、マッチした文字列を \1 、\2 ... で表す。 各々、最初の ( ) にマッチした文字列、第二の ( ) にマッチした文字列 ... を意味する。

正規表現の中に ( ) が使用されていなくてもマッチした文字列は記号 `&' で表される。例えば文字列 bob は 正規表現 [^!@]+ にマッチする。この場合 & は bob を表す。

正規表現で使用される特殊文字を本来の意味に使う時には、その文字の前に '\' を付ける必要がある。例えば文字 `.' は 正規表現では \. で表現する。

sendmail の変換規則に実際に使われる事は無いであろうが、正規表現ではさらに以下の文字が特殊な意味に使われる:

これらの解説は他書に任せる。(例えば文献[2] に解説されている。)

以上で sendmail の変換規則は理解できる。


変換規則 - rewrite -

sendmail が参照するメールアドレスの変換規則は /mail/lib/rewrite に記述されている。

rewrite を眺めて見よう。

---------------- /mail/lib/rewrite----------------------
# case conversion for postmaster
Postmaster      alias           postmaster

# local mail
[^!@]+          translate       "/bin/upas/aliasmail '&'"
local!(.*)      >>              /mail/box/\1/mbox
\l!(.*)         alias           \1
(ar|ar.aichi-u.ac.jp)!(.*)      alias           \2

# we can be just as complicated as bSD sendmail...
# convert source domain address to a chain a@b@c@d...
@([^@!,]*):([^!@]*)@([^!]*)     alias   \2@\3@\1
@([^!]*),@([^!@,]*):([^!@]*)@([^!]*)    alias   @\1:\3@\4@\2

# convert a chain a@b@c@d... to ...d!c!b!a
([^@]+)@([^@]+)@(.+)    alias   \2!\1@\3
([^@]+)@([^@]+)         alias   \2!\1

# /mail/lib/remotemail will take care of gating to systems we don't know
([^!]*)!(.*)      |     "/mail/lib/qmail '\s' 'net!\1'" "'\2'"
---------------------------------------------------------
このファイルに於て、最初のフィールドは送信アドレスを表している。送信アドレスの多様なパターンは正規表現によって表現されている。ここに現われる \l (エル)はホスト名を表している。
送信アドレスと正規表現のマッチングはファイルの最初の行から順次検査される。

第二のフィールドは4つの値をとり、それらは処理の種類を表している。即ち、

である。送信アドレスは /mail/lib/rewrite に記述されたルールに従って再帰的に変換され、最終的には ">>" または "|" による処理を受ける。(そして終わる。)

変換規則は一般に 3 つのフィールドを持つが、第二フィールドに "|" が使用された場合にのみ第四フィールドを許す。第四フィールドの役割は極めて特殊である。 (外部へのメール配送を行う /mail/lib/qmail の実行を合理的に行うために使われている。)

/mail/lib/rewrite はサンプルとしての性格を持ち、厳格さを犠牲にしている事に注意する。例えば

\l!(.*)         alias           \1
(ar|ar.aichi-u.ac.jp)!(.*)      alias           \2
のペアは次の様に一行で書く事もできる。
(\l|\l\.aichi-u\.ac\.jp)!(.*)      alias           \2


幾つかの例

bob 宛ての alice のメールは...

アドレス bob は正規表現 [^!@]+ にマッチし、変換規則 によって が実行される。('&' は [^!@]+ にマッチした文字列、この場合は bob を意味する事を思い出そう。)

この後 2 つのケースが考えられる。bob が別名ファイルに登録されている場合とされていない場合である。

登録されている場合には
/bin/upas/aliasmail bob
は bob の別名、例えば bob@plan9.bell-babs.com を出力する。
登録されていない場合には local!bob を出力する。

これらの各々のケースに応じて送信アドレスは bob@plan9.bell-babs.com または local!bob に置き換えられる。

bob@plan9.bell-babs.com 宛ての alice のメールは...

bob@plan9.bell-babs.com 宛ての alice の メールは正規表現([^@]+)@([^@]+) にマッチし、変換規則 によって plan9.bell-labs.com!bob に変換される。そしてこのアドレスは正規表現 ([^!]*)!(.*) にマッチする。従ってこのメールは変換規則
に従って処理される。ここで "|" の右辺に現われる '\s' は差出人のアドレス(この場合には alice )を表している。即ちメールは によって処理される。

local!bob 宛ての alice のメールは...

アドレス local!bob は正規表現 local!(.*) にマッチする。従って変換規則
local!(.*)      >>              /mail/box/\1/mbox
が適用される。 この場合 \1 は bob なので、このメールは /mail/box/bob/mbox に追加される。

alice@ar.aichi-u.ac.jp 宛ての外からのメールは...

alice@ar.aichi-u.ac.jp は正規表現 ([^@]+)@([^@]+) にマッチし、変換規則 によって ar.aichi-u.ac.jp!alice に変換される。そしてこのアドレスは 正規表現 (ar|ar.aichi-u.ac.jp)!(.*) にマッチし、変換規則 が適用される。この場合 \2 は alice を意味しているのでアドレスは alice に変換される。この後は bob へのメールと同様な経過を辿る。

ルートアドレスの処理は...

ルートアドレスは2つの行
@([^@!,]*):([^!@]*)@([^!]*)     alias   \2@\3@\1
@([^!]*),@([^!@,]*):([^!@]*)@([^!]*)    alias   @\1:\3@\4@\2
によって処理される。 A,b,C,D をホストのアドレス、alice をユーザ名として、ルートアドレス
@A,@b,@C:alice@D
がどのように処理されるかを追って見よう。

アドレス @A,@b,@C:alice@D は正規表現 @([^!]*),@([^!@,]*):([^!@]*)@([^!]*) にマッチし、

	\1 = A,@b
	\2 = C
	\3 = alice
	\4 = D
である。 従って第二の行に従って
@A,@b:alice@D@C
と変換される。これはさらに正規表現 @([^!]*),@([^!@,]*):([^!@]*)@([^!]*) にマッチし、
	\1 = A
	\2 = b
	\3 = alice
	\4 = D@C
である。 従って第二の行に従って
@A:alice@D@C@b
と変換される。そしてこれは正規表現 @([^@!,]*):([^!@]*)@([^!]*) にマッチし、
	\1 = A
	\2 = alice
	\3 = D@C@b
である。従って第一の行に従って
alice@D@C@b@A
と変換される。

インターネット形式からUUCP形式への変換は...

インターネット形式のアドレスは
([^@]+)@([^@]+)@(.+)    alias   \2!\1@\3
([^@]+)@([^@]+)         alias   \2!\1
によって Plan9 の標準形式に変換される。

A,b,C,D をホストのアドレス、alice をユーザ名として、アドレス
alice@D@C@b@A
がどのように処理されるかを追って見よう。

アドレス alice@D@C@b@A は正規表現 ([^@]+)@([^@]+)@(.+) にマッチし、

	\1 = alice
	\2 = D
	\3 = C@b@A
である。 従って第一の行に従って
D!alice@C@b@A
と変換される。このアドレスはさらに正規表現 ([^@]+)@([^@]+)@(.+) にマッチし、
	\1 = D!alice
	\2 = C
	\3 = b@A
である。 従って第一の行に従って
C!D!alice@b@A
と変換される。このアドレスはさらに正規表現 ([^@]+)@([^@]+)@(.+) にマッチし、
	\1 = C!D!alice
	\2 = b
	\3 = A
である。 従って第一の行に従って
b!C!D!alice@A
と変換される。このアドレスはさらに正規表現 ([^@]+)@([^@]+) にマッチし、
	\1 = b!C!D!alice
	\2 = A
である。従って第二の行に従って
A!b!C!D!alice
と変換される。

発送エージェント qmail

qmail はメールを他のホストへ発送する。qmail は簡単なスクリプトであり、その内容は次の様になっている。
------------------------------- qmail --------------------------------
#!/bin/rc
sender=$1
shift
addr=$1
shift

qer /mail/queue mail $sender $addr $* && { runq /mail/queue /mail/lib/remotemail/dev/null >[2=1] & exit 0}
--------------------------------------------------------------------
このスクリプトには明示的な変数 (sender と addr) と引数の残りを表す変数 $* が含まれている。

変数 sender はメールの差出人のメールアドレスである。差出人を alice とせよ。alice が ローカルホストのユーザであれば sender の値は単に "alice" である。他方このメールが他のホスト例えば同一ドメインの venus から発送されて来た場合には sender の値は "venus!alice" となるが、外部のドメインの research.att.com から発送されて来た場合には "research.att.com!alice" となる。

変数 addr はメールの発送先のホストのアドレスである。 alice がメールを bob@plan9.bell-labs.com に出した場合には addr の値は "plan9.bell-labs.com" になる。そして "bob" は qmail の残りの引数 $* で与えられる。

読者は気付いているかも知れないが、qmail は誰のメールでも指定されたアドレスへ発送する。即ち他のホストから来たメールも発送するのである。これはリレーホスト(メールホスト)としての設定である。近年 SPAM が流行しておりリレーホストは SPAM に悪用されている。悪用されるのがイヤなら qmail に手を加えよう。

------------------------------- qmail --------------------------------
#!/bin/rc
sender=$1
shift
addr=$1
shift

if(test -d /mail/box/$sender)
	qer /mail/queue mail $sender $addr $* && { runq /mail/queue /mail/lib/remotemail/dev/null >[2=1] & exit 0}
if not
	cat > /dev/null
--------------------------------------------------------------------
こうすればメールボックスを持っているローカルなユーザだけがメールを外へ出す事ができる。

筆者の qmail はもっと複雑である。
筆者は自宅に ISDN ルータを備えている。自宅からメールを出す場合にメールの発信アドレスが自宅のマシンになっていると困るのだ。メールがリターンされた場合に受け取るマシンが存在しないのである。自宅からメールを出した場合にもメールを大学のマシン ar から出した事にしておけば問題は解決する。(自宅のマシンのメールホストを ar に設定する事。)

自宅のマシン名を mars とする。
------------------------------- qmail --------------------------------
#!/bin/rc
sender=$1
shift
addr=$1
shift

OK=0
if(~ $sender mars!*){
        ifs0=$ifs
        ifs='!'
        s=`{echo -n $sender}
        ifs=$ifs0
        sender=$s(2)
        switch($sender){
        case kenji
                sender=arisawa
        }
        OK=1
}

if(test -d /mail/box/$sender)
        OK=1

if(~ $OK 1){
awk '
/^From |^From: |^received:|^Message-Id:|^Date:|^To:|^received:/{f=1;next}
/^[ \t]/ && f==1 {next}
/^$/{ while (getline > 0) print $0}
/.+/{f=0;print $0}
' |\
qer /mail/queue mail $sender $addr $* && { runq /mail/queue /mail/lib/remotemail/dev/null >[2=1] & exit 0}
}
if not{
echo '-------------' $sender $addr $* '-------------' >> /sys/log/junk
cat >> /sys/log/junk
}
--------------------------------------------------------------------
ここで
if(~ $sender mars!*){
mars!* の部分は自宅のマシンに付けた名前に依存する。(筆者の場合は実際には複数のマシンを処理している。)
        switch($sender){
でユーザ名を変換している。(筆者の場合には家庭のマシンでのユーザ名は kenji であるが、大学のマシンでは arisawa である。複数の家族が利用する場合にはもっと複雑になるはずだ。)

SPAM に対してはそれを捨てないで

echo '-------------' $sender $addr $* '-------------' >> /sys/log/junk
cat >> /sys/log/junk
で記録を採っている。

文献

[1] bryan Costales/Eric Allman/Neil rickert, 村井純監訳「sendmail 解説」(インターナショナル・トムソン・パブリッシング・ジャパン/オーム社,1994)
[2] Dale Doughterty, 福崎俊博訳「sed & awk プログラミング」(アスキー出版局, 1995)