mailmanのバウンスメッセージ処理について興味があったので調べてみました。
はじめに、mailmanにはML購読ユーザにメールが一定期間届かない場合、自動退会する機能がついていますが、その処理はユーザからのバウンスメッセージを元に動作します。
「ユーザからのバウンスメッセージを元に動作する」と一口に書けばそれまでですが、インターネットでのバウンスメッセージには様々な形式があり、それぞれのバウンスメッセージを正しく解析するのは容易ではありません。sendmail/qmail/postfix/yahoo/exchange等様々がMTAが応答するバウンスメッセージはばらばらですから。
で、mailmanがこれら雑多なバウンスメッセージをどうやって判定しているかというと、MTAが出す個別のバウンスメッセージにそれぞれ対応しているようです。
実際の処理を見てみましょう。
これらバウンスメッセージの処理はBounceRunnerというmailmanへのバウンスメッセージを解析するデーモンプログラムで処理されています。
BounceRunnerではバウンスメールを解析し、ループ等の検出を行った後に、それぞれのMTAに対応したバウンスメッセージの処理にディスパッチしています。
- Mailman/Queue/BounceRunner.py
227 # Try VERP detection first, since it's quick and easy
228 addrs = verp_bounce(mlist, msg)
229 if addrs:
230 # We have an address, but check if the message is non-fatal.
231 if BouncerAPI.ScanMessages(mlist, msg) is BouncerAPI.Stop:
232 return
233 else:
234 # See if this was a probe message.
235 token = verp_probe(mlist, msg)
236 if token:
237 self._probe_bounce(mlist, token)
238 return
239 # That didn't give us anything useful, so try the old fashion
240 # bounce matching modules.
241 addrs = BouncerAPI.ScanMessages(mlist, msg)
242 if addrs is BouncerAPI.Stop:
243 # This is a recognized, non-fatal notice. Ignore it.
244 return
この231/241行目に出てくるBouncerAPI.ScanMessages()というメソッドがポイントです。
このメソッドは、mailmanへのバウンスメッセージかどうかをMTA毎に解析します。(実際にはバウンスメッセージに含まれているバウンス元ユーザを抽出する処理を行っています)
- Mailman/Bouncers/BouncerAPI.py
61 # msg must be a mimetools.Message
62 def ScanMessages(mlist, msg):
63 for module in BOUNCE_PIPELINE:
64 modname = 'Mailman.Bouncers.' + module
65 __import__(modname)
66 addrs = sys.modules[modname].process(msg)
67 if addrs:
68 # Return addrs even if it is Stop. BounceRunner needs this info.
69 return addrs
70 return []
63行目-69行目でMTA毎の個別処理についてループしていますが、このループ一覧は以下になります。
- Mailman/Bouncers/BouncerAPI.py
39 BOUNCE_PIPELINE = [
40 'DSN',
41 'Qmail',
42 'Postfix',
43 'Yahoo',
44 'Caiwireless',
45 'Exchange',
46 'Exim',
47 'Netscape',
48 'Compuserve',
49 'Microsoft',
50 'GroupWise',
51 'SMTP32',
52 'SimpleMatch',
53 'SimpleWarning',
54 'Yale',
55 'LLNL',
56 'AOL',
57 ]
これを見る限り、DSN(Delivery Status Notifycation)から始まり、qmail->postfix->yahoo …と個別に判定を加えているようです。
試しにDSNを見てみましょう。PGをざっと見る限り、マルチパートで且つ、DSNの書式に則った応答の場合は、バウンスメッセージからバウンス応答したユーザ情報を抜き出す作りになっているようです。
- Mailman/Bouncers/DSN.py
64 for header in ('original-recipient', 'final-recipient'):
65 for k, v in msgblock.get_params([], header):
66 if k.lower() == 'rfc822':
67 foundp = True
68 else:
69 params.append(k)
70 if foundp:
71 # Note that params should already be unquoted.
72 addrs.extend(params)
73 break
74 else:
75 # MAS: This is a kludge, but SMTP-GATEWAY01.intra.home.dk
76 # has a final-recipient with an angle-addr and no
77 # address-type parameter at all. Non-compliant, but ...
78 for param in params:
79 if param.startswith('<') and param.endswith('>'):
80 addrs.append(param[1:-1])
MTA毎に個別の処理を書いてはいますが、本当に個別となる部分だけを抽出して書いているので、かなりきれいなプログラムだと思いました。すごいな、mailman。