qmail の RFC 違反

違反してるからといって、それを糾弾するつもりも、使うなというつもりもないし、qmail 以外の MTA がまったく違反をしていないというつもりもない。ただ、他の MTA と比べてわりと目につきやすいところでやってるので、とりあえず気がついたところをメモしてみたもの。さらに、Qmail bugs and wishlist. というページの情報も加えてある。読めばわかるが、qmail-pop3d がかなり悲惨。

あんまり詳しく調査しているわけではないので鵜呑みにはしないように。間違いや追加情報があれば教えてください。

qmail-send: ダブルバウンスの envelope sender が不正

詳細は別ページ。ダブルバウンスの転送先が Postfix（2.1 以降）や Sendmail X だと実害発生。

qmail-send: VERP のローカルパートの長さ超過

MAIL FROM: pre-@host-@[]、RCPT TO: recip@domain でメールが送られると、qmail-send が MAIL FROM: pre-recip=domain@host、RCPT TO: recip@domain とエンベロープを書き換えて送信する。これが VERP。

RFC2821 #4.5.3.1 にはメールアドレスのローカルパートは64文字までという規定がある。受信者の recip が64文字以下であっても、@domain 込みのメールアドレス全体で64文字以上になる場合、VERP で書き換えられた後のローカルパート pre-recip=domain は64文字以上になる。よって RFC 違反。VERP は postfix でもサポートされている。

qmail-remote: 8BITMIME

SMTP では 8bit な文字を送ることはできない。そのような場合は ESMTP の 8BITMIME 拡張を使わなければならない。しかし、qmail-remote は 8BITMIME ではなく素の SMTP でこれを送ってしまう。というか、そもそも ESMTP を知らない。

qmail-smtpd の方は 8BITMIME をサポートしている。そのため、qmail 以外の ESMTP を喋れる MTA が 8bit な文字を含んだメールを qmail に対して送るときは 8BITMIME を使って違反なく送れる。しかし、そのメールを他のホストに中継しようとするとき 8BITMIME を使わず送ってしまう。たいていの MTA は寛容に受けとってくれるが、ものによってはメッセージの 8bit 目を落とされたりエラーにされたりするかもしれない。

また、これは違反ではないが ESMTP 関連だと、qmail-remote が ESMTP SIZE 拡張が使えないために、長大なメールを送ってからサイズ超過でエラーにされることがある。エラーにされるにしても、SIZE が使えれば DATA で送る前に MAIL FROM の段階で検出できるのに、それができないのでネットワークの無駄となる。qmail-smtpd も SIZE はサポートしていないので、databytes を越えたメールをエラーにするときも MAIL FROM の段階では不可能で、いったんすべて受信しなければならない。やっぱりネットワークの浪費。

qmail-remote: バックアップ MX に配送しない

メールの配送先サーバは必ずしもひとつとはかぎらない。MX を複数設定したり、単にラウンドロビンだったりして、配送先の候補がいくつもあることが多い。もちろん、その候補をどんな順番で配送試行するのかは RFC2821 #5 に規定があるのだが、qmail はその最初のひとつしか試さない。もっとも高い優先度のホストからエラー応答が返ってきたならば、次の優先度のホストに配送しなければならないのだが、それをしないで時間をおいて同じホストに再送しようとする。

SMTP でエラー応答が返るのではなく、TCP RST で SMTP 接続自体を蹴られたり、タイムアウトで接続できなかった場合には、正しく優先度次点のホストに配送するが、TCP FIN が返ってきて接続失敗した場合には、やはり同じホストに再送してしまうようだ。

わりと致命的なので、パッチを当てておいた方がいいかもしれない。ただし、このパッチでも TCP FIN の問題はそのままのように見える（未確認）。

qmail-pop3d: LIST/STAT で返されるメールのサイズが不正

SMTP や POP3 では改行を CR+LF の2バイトであらわすが、qmail-smtpd はメールを受けとるときにこれを LF の1バイトに変換し、qmail-local はそのままメールボックスに書き込む。qmail-pop3d はこの変換で出た差を考慮せずファイルサイズをそのまま LIST/STAT の応答で返してしまう。つまり、LIST/STAT で返るバイト数が RFC の規定よりも行数と同じバイト数だけ小さくなる。

一部メーラーにあるXXバイト以上のメールをダウンロードしない、などの機能は LIST の応答を利用するが、qmail-pop3d のこの仕様のため、無視されるべきメールも対象になってしまう。

qmail-pop3d: よけいな空行が付加される

末尾に本来存在していない空行を挿入してメッセージを破壊する。これは仕様である。

TOP ではメッセージ先頭から指定した行数を取得できるが、この仕様のため、たとえば10行要求した場合でも応答は11行になる。しかもその11行目は本来のメッセージの11行目とは異なっている。

もちろん、RETR でメッセージ全体を取得したときにも末尾に空行が付加される。末尾ならそれほど問題がないように思えるかもしれないが、RFC2046 #5.2.2 の message/partial、いわゆる分割メールを結合したときに、メッセージの途中、分割されていた境界部分に qmail-pop3d に付加された空行が残ってしまう。添付ファイルのデコードや電子署名されていたメールの正当性の検証に問題が出る可能性がある。

というか、TOP と RETR の区別がまったくないんですが。行数指定なしで TOP すると RETR と同じ動作するし、行数指定して RETR すると TOP になる。-ERR にするべきだろ、これ。

qmail-pop3d: LAST の応答がデタラメ

まずはじめに指摘しておくと、POP3 のプロトコルを定めているのは現在 RFC1939 であるが、これに至るまで、RFC1081 → 1225 → 1460 → 1725 → 1939 と何度も改訂が繰り返されている。LAST なるコマンドが存在していたのは、このうち RFC1460 までであり、RFC1725 (1994/11) 以降は POP3 というプロトコルから LAST は削除されている。しかし、多くの POP3 サーバは過去からの互換性を考慮して LAST を実装している。qmail-pop3d もそのひとつである。

LAST とはメールボックス中のメッセージのうち、最後にアクセスしたメッセージの番号を返す、すなわち既読ポインタを返すコマンドである。LAST のない現在の POP3 は UIDL を使ってクライアント側で未読管理をすることになっているが（ただし、UIDL はオプショナル）、RFC1460 の POP3 では LAST を使うことでサーバ側で未読管理をすることができていた。

たとえば、RETR 5 として、メールボックス中の5通目を取得した後で LAST コマンドを送ると、本来は +OK 5 のように、最後にアクセスしたメッセージの番号が返される。しかし、qmail-pop3d は常に 0、つまり1通目から未読メッセージ、という応答を返す。1通目を削除 (DELE 1) すると、以降は +OK 1、つまり2通目から未読という応答を返すようになっているが、肝心の RETR では LAST の応答は変わらないし、ログアウトすると 0 にリセットされてしまうので、既読管理という用途には使えない。未読メールを読み出すのに LAST コマンドを使うクライアントの場合、qmail-pop3d が毎回リセットしてしまうので、一度読んだメールを毎回取得することになってしまう。

今どきのクライアントでは LAST を使う方がおかしいのだが、だからといってそれに対してムチャクチャな応答を返していいわけではない。古いプロトコルとの互換性があるように見えて、実際の動作は互換とはほど遠い。こんなデタラメをするくらいならばサポートしない方がマシである。

qmail-pop3d: DELE の後の STAT でおかしな値を返す

RFC 違反というより、明らかなバグ。実例。

STAT +OK 3 8984 DELE 1 +OK STAT +OK 3 6692

3通8984バイトのメールから1通削除したら、サイズは削除した分だけ減ってるのにメッセージ総数は変わっていない。

改行コードについて

SMTP/POP3 の改行コードは CR+LF と規定されているが、qmail は内部的には LF を使っている。そのため、随所でそれぞれの変換をおこなっている。が、その変換に中途半端なところがあるので RFC に違反したり、違反ではなくてもよろしくない現象が起きたりする。

qmail-smtpd CR+LF を LF に変換する。単独の LF は一時エラーにする。 qmail-inject 改行コードの変換はしない。ただし、ヘッダの追加、書き換えのあった行については LF になる。 qmail-local 改行コードの変換はしない。 qmail-remote LF を CR+LF に変換する。すでに CR+LF だった場合は CR+CR+LF になる。 qmail-pop3d RETR/TOP に対しては LF を CR+LF に変換して返す。LIST/STAT には変換前のサイズを返す(上述)。

たとえば、あらかじめ改行が CR+LF にして用意しておいたメッセージを qmail-inject に食わせると、改行コードが CR+CR+LF になって外部に送られるし、ローカルに送るとひとつのメッセージに CR+LF と LF が混在してしまう。なんでわざわざこんなことしてるのかさっぱり理由がわからん。

なお、ローカルのメールボックスの形式については RFC の規定はないので、内部的にはどんな形式で処理しようが任意。ただし、それを別のプログラムとやりとりするときには RFC2821(SMTP)/RFC1939(POP3)/RFC2822(Internet Message Format) に準拠した形式に変換しないとダメ。もっとも、RFC じゃなくても、これこれこういう仕様です、と定めておいて、それにアクセスするプログラムがすべてそれに従うようになっていれば、必ずしも RFC に準拠する必要はない。要は、独自仕様だろうが RFC だろうが、従うべき仕様をきっちり遵守していればいい。

maildir(5) のドキュメントを読むと RFC822 の形式で保存するとあるが、上述のように実際には改行は LF であり、RFC822 で定められた CR+LF ではない。仕様(ドキュメント)と実装が一致していない。Maildir を最初に採用した qmail が LF なので、postfix や procmail や maildrop などの後追い実装はすべて、互換性を取るため CR+LF ではなく LF で保存するようになっている。

<y@maya.st>