[Site]Mailmanをrecipient_delimiter=-なPostfixで動かす

週末ということで、apt-getの後ほとんど進展がなかったMailmanのHack。

Mailmanって?

Mailmanってのは簡単に言えばメーリングリスト。特徴はWebとの連携が強くて、WebからのSubscribeだとか、アーカイブ閲覧だとか、管理だとかができてしまう、eGroupsのオープンソース版みたいなもの。

Postfixにネイティブに対応してて、aliasファイルはpostmapを自動実行してくれたり、バウンスメールを自動処理してくれたりと非常に頼もしいのだが……。一点最大の問題点が。

以下暇じゃない人はすっ飛ばして「Mailmanをrecipient_delimiter=-なPostfixで動かす最善の方法」をどうぞ。

拡張アドレス

qmailから(SPAMとかDoubleBounceに疲れ果てて)Postfixに流れた人なら判るだろうけど、拡張アドレスという便利な機能があって、someuserというユーザにsomeuser@domainは普通だけど、someuser-test@domainとかsomeuser-alert@domainというメールも配送させるという機能。qmailなら.qmail-testとか、Postfixなら.forward-testとかでprocmail呼んだり別のメールボックスに振り分けたりできた訳だ。
で、そのデリミタとして"-"が使われていたんだけど、Postfixになって(デフォルトが)"+"になってしまった。当然ながらデフォルトをえいやっと変えて(recipient_delimiter = -)前に使ってたメールアドレスをそのまま使っていた訳で。

Mailmanの利用するメールアドレス

で、その拡張アドレスが何だというと、Mailmanはデリミタが"+"であることを前提として作られていて、testというMLを作ると、以下のようなaliasを自動的に生成してくれたりする。

test:             "|/var/lib/mailman/mail/mailman post test"
test-admin:       "|/var/lib/mailman/mail/mailman admin test"
test-bounces:     "|/var/lib/mailman/mail/mailman bounces test"
test-confirm:     "|/var/lib/mailman/mail/mailman confirm test"
test-join:        "|/var/lib/mailman/mail/mailman join test"
test-leave:       "|/var/lib/mailman/mail/mailman leave test"
test-owner:       "|/var/lib/mailman/mail/mailman owner test"
test-request:     "|/var/lib/mailman/mail/mailman request test"
test-subscribe:   "|/var/lib/mailman/mail/mailman subscribe test"
test-unsubscribe: "|/var/lib/mailman/mail/mailman unsubscribe test"

ここまではデリミタが"-"であっても大丈夫。問題はここから。

バウンスメールの扱い

上記test-bouncesというのがエラーメールの戻り先となっている。

Return-Path: <test-bounces+user=domain.jp@mldomain.com>

いわゆるVERPというやつ。配送先でaliasかけたり転送したりしてあるメールがトラブルを起こすと、メーリングリストでは、そのトラブルの原因を探すのにかなり骨を折ることになる(場合があった)。
そのため、エラーメールがそれぞれ「どこ宛に配送されたのか」という情報を返してくれるようにするのがVERPという仕組み(厳密には嘘を言ってるかも)。
Postfixのデフォルトだと"+"がデリミタなので、$userは"test-bounces"で$extensionが"user=domain.jp"となる。
ところがデリミタを"-"にすると、$userは"test"で$extensionが"bounces+user=domain.jp"となる。そのため、バウンスがpost用の処理に廻ってしまうということ。(つまりループするんだと思う)

回避策1

まずは、メーリングリスト専用のサブドメインを作ってやる方法を試す。いわゆるバーチャルドメイン型。連携方法には2種類あって、master.cfのpipeによって処理する方法(先例の2の方)を検討。頑張ってpostfix-to-mailman.pyの先頭にある英文の説明を読む。
海外の情報を元に(DebianMailmanインストール時に勝手に生成した)master.cfの以下の箇所を修正。

mailman   unix  -       n       n       -       -       pipe
  flags=FR user=list argv=/etc/mailman/postfix-to-mailman.py ${nexthop} ${user}

これを

mailman   unix  -       n       n       -       -       pipe
  flags=FR user=list argv=/etc/mailman/postfix-to-mailman.py ${nexthop} ${user}${extension?-}${extension}

こんな感じに。

結末1

warning: file /etc/postfix/master.cf: service mailman: unknown macro name: "extension?-"

Postfixは2.3なのに〜。多分このセマンティクスでは解釈されないんだろうなぁ。(;_;)

回避策2

回避策1の情報にあった、wrapperをかます方を試す。master.cfの以下の箇所を修正。

mailman   unix  -       n       n       -       -       pipe
  flags=FR user=list argv=/etc/mailman/postfix-to-mailman.sh ${nexthop} ${user} ${extension}

こんな感じに。で、postfix-to-mailman.shは

test -z "$3" || DELIM=-
cat | /usr/lib/mailman/bin/postfix-to-mailman.py "$1" "$2$DELIM$3"

ただこれは、あまりにも幼稚*1なので、この方式を使うなら、

test -z "$3" || DELIM=-
exec /usr/lib/mailman/bin/postfix-to-mailman.py "$1" "$2$DELIM$3"

としたほうが、余計なプロセス作らずに済むのでbetter。(といえど、結局master.cfの「${user} ${extension}」の代わりに「${mailbox}」が使えるし、そうなるとwrapper不要になって、結局無意味)

結末2

エラーなし。よしよし。
bouncesの際に渡されている情報を確認すると、「postfix-to-mailman.py mldomain.com test-bounces+user=domain.jp」になってる。Pythonは学習してないので読めないのだが、まぁそれなりに読むと、第2引数が'-bounces'で終わっているかどうかを見てbounces処理に渡してる。ご〜ん。ということは、「postfix-to-mailman.py mldomain.com test-bounces」を渡さなきゃ。ということでNGなり。
ちなみに、回避策1の「${user}${extension?-}${extension}」は「${mailbox}」に置き換え判明。なんにせよこのシェルスクリプトは無意味だった。

回避策3

ならば、Postfixの持つVERP機能を使えば何とかなるのではと、探したところHatuka*nezumiさんの作ったパッチにあるドキュメントからヒントを得て、バウンスメールのenvelope-fromそのものを変えてしまうことで、PostfixがVERPのところを削除してくれたりしないかと思って実験。

結末3

自分のところのPostfixは、「default_verp_delimiters = -=」だったので、

VERP_FORMAT = '%(bounces)s-%(mailbox)s=%(host)s'
VERP_REGEXP = r'^(?P<bounces>[^=]+?)-(?P<mailbox>[^=]+)=(?P<host>[^@]+)@.*$'

とか考えてみたけど、ハイフンが複数出てきた時点で曖昧になるからダメと、main.cfを

default_verp_delimiters = +=

とした上でVERP_FORMATとVERP_REGEXPを元に戻して確認したが、結局NG。なぜだ〜。多分***-***+***=***@***と、ハイフンがあるからだろうなぁ。(先に拡張アドレスを決定してしまうのではないか、と)
(ということは'%(bounces)s~%(mailbox)s=%(host)s'とかにしてもダメなんだろうなぁ)

回避策4

となると、結局どこかで個別処理して不要な部分を切り落とすか、不要な部分を無視するしかない、ということに。

結末4

じゃ、wrapperを何で記述するかといえば、

  • perlはメール配送のたびに呼ばれる事を考えると重すぎるから却下。
  • sh/bashは文字列操作は面倒っぽいし、bashがない人とか考えると却下。
  • C/C++が良さそうだがmakeとか考えると大事過ぎ。却下。

という30秒くらいの思考を経て、googleさんに「python 入門」とか聞いてみる。

それから1時間。できましたっ

Mailmanをrecipient_delimiter=-なPostfixで動かす最善の方法

最善の定義は「俺様の決めたこと」なので、あまり気にしないように。
前提はメーリングリスト専用のメールドメインを用意できること。そしてDebianのapt-getでMailmanを入れていること。他の方法でのインストールで同じpostfix-to-mailman.pyが入るかどうかが判らないので。多分同じだと思うけど。
追加するメーリングリスト専用のメールドメインはml.domain.comとしておく。

バーチャルなサーバで運用する時と同じように設定する。MTA変数をNoneにしておくことで、Postfix専用にaliasを作ったりpostmapしたりというのを抑止する。

DEFAULT_EMAIL_HOST = ml.domain.com
DEFAULT_URL_HOST   = ****.domain.com
add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST)
MTA = None

以下の5行を追加する。

*** postfix-to-mailman.py.orig  2007-11-17 21:01:20.000000000 +0900
--- postfix-to-mailman.py       2007-11-17 21:11:46.000000000 +0900
***************
*** 105,114 ****
--- 105,119 ----
              sys.stderr.write('Did you forget to set '
                               'mailman_destination_recipient_limit=1 '
                               'in main.cf?')
          sys.exit(EX_USAGE)

+     # 'mylist-bounces+member=dom.ain' -> 'mylist-bounces'
+     pluspos = local.find('+')
+     if pluspos > 0:
+         local = local[:pluspos]
+
      # Redirect required addresses to
      if local in ('postmaster', 'abuse', 'mailer-daemon'):
          os.execv("/usr/sbin/sendmail",
                   ("/usr/sbin/sendmail", MailmanOwner))
          sys.exit(0)

以下の行を編集(あるいは追加)。最後の「${mailbox}」に注意。

mailman   unix  -       n       n       -       -       pipe
  flags=FR user=list argv=/etc/mailman/postfix-to-mailman.py ${nexthop} ${mailbox}

以下の項目を編集(あるいは追加)。

recipient_delimiter = - (多分これじゃなくても動くけど、おやくそく)
relay_domains =
        $mydestination
        ml.domain.com
transport_maps =
        hash:$config_directory/transport
mailman_destination_recipient_limit = 1

新規作成する。既存のものがある人は上記main.cfのものも含めて適当な名前に変更してよろし。

ml.domain.com        mailman:

作成後postmap transport とかして、/etc/postfix/transport.dbを作っておくこと。



ちなみに、このあとpostfixmailmanもrestartしてください。main.cfとかmaster.cfはpostfixのreloadやrestart前にも読まれる可能性があるので注意したほうが良かったり。

ふぅ。長かった。

*1:コメント参照