管理の小技集

キューの状態確認

root# /var/qmail/bin/qmail-qstat
root# /var/qmail/bin/qmail-qread

qmHandle (perlスクリプト) を使うともう少し詳細なアウトプットも得られる。

※ qmHandle1.2.0 に付属するREADMEは 1.1.1 の時から更新されていないようだ。-v オプションはもう無く、 -m に変更となっている。

特定キューの削除

メッセージは、いくつかに分解された状態でキューに蓄積されている。キューディレクトリが /var/qmail/queue で、qmail-qread で確認したキューナンバーが 555 だったとすると、 これらのファイルが存在するはず:

queue/mess/[0-22]/555 メッセージ
queue/todo/[0-22]/555 エンベロープの from と to 情報
queue/intd/[0-22]/555 エンベロープの from と to 情報 (qmail-queueが処理中の時)
queue/info/[0-22]/555 エンベロープの sender アドレス
queue/local/[0-22]/555 ローカル宛に処理待ちのエンベロープ受取人アドレス
queue/remote/[0-22]/555 リモート宛に処理待ちのエンベロープ受取人アドレス
queue/bounce/[0-22]/555 回復の見込みのない (permanent な) デリバリーエラー

[0-22] は 0 から 22 のどれかの意。local, remote はメッセージの状態によっていずれか一方。 qmail-send, qmail-smtpd, qmail-pop3d をすべて止めてから、これらのファイルを削除すればよい。簡単なスクリプト (例:qqclean) を組むか、qmHandle で処理できる。

古いメールの削除

rmoldmailスクリプト

キューでなく、既にユーザの Maildir に届いたメールの削除の話。世の中のシステム管理者の 5割は忙しすぎ、残りの 4割の管理者はモノグサで、ほとんどのサーバで例えば root 宛のログメールなどが溜まりっぱなしになっているという惨状がある。そこで、期限を決めて、古いメールは自動削除されるように仕掛けをしておこう。これも 1メール 1ファイル形式の Maildir だからこそ成せる技だ。処理自体には find を使う。自分の Maildir にある 10日以上古いメールを削除するとすれば下記のようになる;

find ~/Maildir/ -type f -mtime +10 -exec rm -f '{}' ';'

これを cron で週に一度 (やりたければ毎日でもいいが) 実行してやればいい。ただしユーザ何人分も仕掛けるのはいちいちメンドクサく、ミスの元にもなるので、ちょっと工夫して汎用的に使える rmoldmail というスクリプトを書いた。 /usr/local/bin にでも置いて使って頂きたい。使い方は、ヘルプにも書いたが;

rmoldmail [-V] HOW_DAYS_OLD [OWNER]

OWNER 引数は省略でき、省略した場合にはスクリプト実行ユーザの Maildir が対象となる。例えば、
自分のメールのうちで 7日以上古いメールを毎週日曜日の午前 3時 54分に削除する cronジョブは;

54 3 * * sun /usr/local/bin/rmoldmail 7

hoge というユーザの持つメールのうち 10日以上古いものを毎日午前 4時 31分に削除する、 root 所有のジョブ定義は;

31 4 * * * /usr/local/bin/rmoldmail 10 hoge

Maildir が例外的なディレクトリにある場合には、OWNER 部に `/' で始まる絶対指定のディレクトリ名を指定し、例えば;

31 4 * * * /usr/local/bin/rmoldmail 10 /home/test/hoge

とすると、この場合 rmoldmail/home/test/hoge/Maildir を探す。`/Maildir' はスクリプトが足すので、引数では付けてはいけない。また、 qmail-vida や 「システムアカウントを増やさずにメールユーザを作る」 の手法を使ってバーチャルメールユーザを管理している場合には、 -V オプションを含めると、スクリプト冒頭の変数 $VMDIR を基底ディレクトリとして指定ユーザの Maildir を探す。詳しくはスクリプト内のコメントを参照。

しかし、システムで多数のバーチャル・メールユーザを管理している場合、一人一人に対する cronジョブを登録するのは面倒だ。そこをカバーするために作ったのが、下の rmmail_virtual スクリプトだ。

rmmail_virtualスクリプト

これは、前出の rmoldmail と併用して、qmail のユーザ管理ファイルからバーチャルメールユーザを読み取り、各ユーザの古いメールを一括して削除できるシェルスクリプト。上記の rmoldmail スクリプトを /usr/local/bin/ へ配備した上で、こちらのスクリプトを cron のジョブに登録するといい (例えば、 /etc/cron.daily ディレクトリへコピー)。環境によって選択できるように、ふた通りのバージョンを作成した。

いずれも、前述の rmoldmail スクリプトを
/usr/local/bin/rmoldmail -V 21 USER
というコマンドをユーザの数だけ繰り返して呼ぶ仕組みになっている。デフォルトは一応 21日 (3週間) にしているが、rmmail_virtual 冒頭の変数で適宜変更していただきたい。引数は何も要らないが、最低限、

※ユーザ管理ファイルからバーチャルユーザを捜すルーティンには awk 版と egrep+cut 版が書いてあり、後者はコメントアウトしてある。 システム環境などによって awk ルーティンでうまくいかない場合は、後者へ切り替えてみるといいだろう。
※エクステンションアドレス (assignファイルでの +foo-:...) には対応していない。

動作テストのためのコマンド

送信テスト (local - local)

user$ echo to: hoshu | /var/qmail/bin/qmail-inject

送信テスト (local - remote)

user$ echo to: me@my_isp_mail | /var/qmail/bin/qmail-inject

送信テスト (local bounce)

user$ echo to: nonexistent | /var/qmail/bin/qmail-inject

送信テスト (double bounce)

root# /var/qmail/bin/qmail-inject -f nonexistent
to: another_nonexistent
subject: test
 
This is a test.
<Ctrl-d>

送信テスト 1 (telnet を使っての SMTP 経由)

user$ telnet 127.0.0.1 25
helo localhost.
mail from: me
rcpt to: hoshu
data
Subject: test

This is a test.
.
quit

送信テスト 2 (sendmail コマンドを使って)

root# /usr/sbin/sedmail -t -i -froot@localhost
From: root@localhost
To: hoshu
Subject: test

This is a test.

<Ctrl-d>

送信テスト 3 (SMTP-AUTH 平文認証テスト)

telnet 127.0.0.1 25
ehlo localhost.
auth plain
*****************
quit

**** の部分の文字列は、userID\0userID\0passwd を Base64 エンコードした文字列。例えば、シェル上から Perl を使って簡単に作れる。userID を hanako, パスワードが passwd だとすると:

perl -MMIME::Base64 -e '
$str = "hanako\0hanako\0passwd";
print encode_base64($str,""),"\n";'

とすれば (※) エンコード文字列が得られるので、それを貼り付ければよい。ただしいちいちコマンドするのは面倒くさいので、こういったスクリプトファイル (smtpauthstr.pl) にしておくと楽だ。なお、Perl に MIME::Base64 モジュールをインストールしてない場合は、何かと便利なのでこの機会にインストールしてしまおう。Perlモジュールのインストール解説は Perlのページで網羅している。

※ SMTP-AUTH で求めるユーザID がメールアドレス (QMAILでは希だが) である場合は、"hanako\@mail.hoge.cxm\0..." のようにアットマークをエスケープしなくてはならない。

送信テスト 4 (日本語メール) - jmailsend.pl

なるべく RFC に沿ったフォーマットで日本語のテストメールを簡単に送れる Perl スクリプトを作成した。内部的にはローカル上の sendmail コマンドを使って送信を行っている。

jmailsend.pl

使い方:

jmailsend.pl [-e (jis|sjis|euc|utf8)] [-c N] [-t RECIPIENT] [-h]

スクリプト自体の冒頭に、もっと詳しく使い方が書いてある。文面は固定だが、-e オプションで指定することによって JIS, Shift_JIS, EUC-JP, UTF-8 から文字コードを選べる。 -c オプションで数を指定すれば連続して送るので (宛先はひとつ)、メールサーバの負荷テストにも使える。それらのデフォルト値は冒頭附近の変数で変更することもできる。今のところ SMTP-AUTH には対応していない。

受信テスト 1 (telnet を使っての POP3)

telnet 127.0.0.1 110
+OK <1234.1096123456@hoge.cxm>
user hanako
+OK
pass passwd
+OK
stat
+OK 1 809
list
+OK
1 809
.
retr 1
(内容が表示される)
dele 1
+OK
quit

受信テスト 2 (APOP)

telnet 127.0.0.1 110
+OK <1234.1096123456@mail.hoge.cxm>
apop hanako ****************
+OK
stat
(以下、通常の POP3 と同じ)

**** の部分の仕組みは、接続時にサーバが送ってきたチャレンジ文字列 + パスワードを、MD5 でハッシュした 16 オクテット (16進数[小文字] x 32 文字) の値。ちなみに、チャレンジ文字列は、UNIX 上の MTA では通例 <PID.TimeStamp@server> が使われることが多い。上記の例で、生のパスワードが passwd だとすれば、
<1234.1096123456@mail.hoge.cxm>passwd

を ("< >" も含めて) MD5 ハッシュした値を **** に入れれば正解となる。RFC1939 (Post Office Protocol - Version 3)

Fedora Core 1 でやった時には、 openssl を使って発生した値ではうまくいかなかったので Perl のモジュール Digest::MD5 を使った。そのスクリプトを、つまらないものだが参考のため置いておく (mkdigest)。ターミナル上で:

mkdigest "<1234.1096123456@mail.hoge.cxm>passwd"

といった具合に使っていただきたい。素のままだと "<" がリダイレクト記号と解釈されてしまうので、必ずダブルクォートで囲むこと。 Perl モジュール Digest::MD5 はかなり一般的なようだが、万が一インストールされていない場合はインストールすべし。Perlモジュールのインストール解説は Perlのページ

メールフィルタの作り方

dot-qmail の項で述べたように、管轄の .qmailファイルに "|program " と書いておけば、そのユーザの Maildir にメールが配信されるや否や (正確にはMaidirに入る直前)、qmail は program にその内容を渡してくれる。program はシェルスクリプトでも perl でも php でも何でもよく、うまく利用すれば、メール内容を処理してデータベースに入力したり、受け取らない (つまり捨てる) ようにしたり、さまざまな処理が実現できる。ユーザ当たりの Maildir 容量に制限を設ける mailquotacheck もこれを応用している。

program は、しかるべき終了コードを返すように書かなくてはならない。program による処理が終わると、制御はまた qmail-local に戻る。その時、qmail-local の挙動を制御するのが「終了コード」だ。

終了コード 意味
0 処理は正常に完了した。qmail-local はそのまま処理を続けろ
99 処理は正常に完了した。qmail-local はもう何もするな
100 permanent なエラー (ハードエラー)
111 エラー (ソフトエラー)。qmail-local は後で再び配送を試みよ

※ 詳しくはman qmail-command せよ。

"|" の動作をテストするPHPスクリプト例

qmail-local は、stdin を通じてprogram にメール内容を渡し、内容の終了は EOF で知らせてくる。以下に、ごく単純なスクリプト例を示す。

#!/usr/bin/php
<?php
$input = fopen('php://stdin', 'r');
$msg = array();
while(! feof($input) ){
    array_push( $msg, fgets($input) );
}
foreach($msg as $line){
    echo $line, "<and>\n";
}
exit(99);
?>

EOF が来るまで stdin からメッセージを1行ずつ読み取り、エコーするだけのプログラムだ。単純に書き出すだけなら、各行を配列に落とし込む処理は無駄でしかないが、文字列を変換や検索/抽出したり、他のルーティンへ変数渡しする処理などに応用しやすいよう、このようなアルゴリズムを採った。また、実用したわけではないが、スクリプト中で sendmail ラッパーに渡す処理を行えば、自動返信も簡単に実現できそうだ。

このスクリプトを例えば /usr/local/sbin/mailtest として置いて実行ビットを立て、テストユーザの .qmail に:

|/usr/local/sbin/mailtest >mailtest.txt
./Maildir/

と書いておけば、受け取ったメールの内容がユーザのメールホームに mailtest.txt として書き出される。 mailtest の終了コードは 99 にしてあるので、qmail-local はここで仕事を放棄するため "./Maildir/" の指示は処理されない。終了コードを 0 にすれば、テキストファイルに加えて通常の Maildir にも配送されるわけだ。