Mailman のメーリングリストのメンバーを LDAP で管理する

## 概要

Mailman で作成したメーリングリストを部局構成員向け同報アドレスとして利用する[^1]場合、部局構成員の交代（新規採用や退職など）に応じて、メーリングリストのメンバー登録を追従する必要がありますが、それなりに手間がかかります。

本稿では、部局構成員が登録された LDAP サーバを利用して、自動的にメーリングリストのメンバー登録を、最新の状態に維持する方法を紹介します。

## インストール手順

インストール手順は、以下の2段階です。

 1. https://github.com/tsuchm/MailmanLDAPMemberships から、`LDAPMemberships.py` をダウンロードして
 `/usr/lib/mailman/Mailman/` にコピーする。
 2. `/var/lib/mailman/lists/メーリングリスト名/extend.py` に設定を記述する。

## 設定例

以下、`/var/lib/mailman/lists/メーリングリスト名/extend.py` に記述する設定例を、いくつか示します。

### 全学生をメンバーとする場合

全学生が (employeeType=student) という検索式で得られる場合の設定例は、以下の通りです。

```py:extend.py
from Mailman.LDAPMemberships import LDAPMemberships

def extend(list):
    ldap = LDAPMemberships(list)
    ldap.ldapsearch = "(employeeType=student)" # 検索条件
    ldap.ldapserver = "ldap.example.net"       # LDAPサーバ名
    ldap.ldapbasedn = "dc=example,dc=net"      # 検索時の base DN
    ldap.ldapbinddn = ''                       # LDAPサーバ接続ユーザ名
    ldap.ldappasswd = ''                       # LDAPサーバ接続パスワード
    list._memberadaptor = ldap
```

### 全学生と事務局代表アドレスをメンバーとする場合

 1. 全学生は (employeeType=student) という検索式で得られる。
 2. 事務室代表アドレス `office@example.net` は、上記検索式では得られないが、必ずメンバーに加えておく。

というような場合の設定例は、以下の通りです。

```py:extend.py
from Mailman.LDAPMemberships import LDAPMemberships

def extend(list):
    ldap = LDAPMemberships(list)
    ldap.ldapsearch = "(employeeType=student)" # 検索条件
    ldap.ldapserver = "ldap.example.net"       # LDAPサーバ名
    ldap.ldapbasedn = "dc=example,dc=net"      # 検索時の base DN
    ldap.ldapbinddn = ''                       # LDAPサーバ接続ユーザ名
    ldap.ldappasswd = ''                       # LDAPサーバ接続パスワード
    ldap.ldappersistentmembers = ['office@example.net']
    list._memberadaptor = ldap
```

### グループに登録されている学生をメンバーとする場合

以下のように LDAP 上にグループが登録されている状態を考えます。

```shell-session
$ ldapsearch -h ldap.example.net -b dc=example.net '(cn=examplegroup)'
dn: cn=examplegroup,ou=groups,dc=example,dc=net
objectClass: groupOfUniqueNames
cn: examplegroup
uniqueMember: uid=alice,ou=users,dc=example,dc=net
uniqueMember: uid=bob,ou=users,dc=example,dc=net
```

このグループに登録されている学生（この例では alice と bob）をメンバーとするには、以下のように設定します。

```py:extend.py
from Mailman.LDAPMemberships import LDAPMemberships

def extend(list):
    ldap = LDAPMemberships(list)
    ldap.ldapsearch = "(cn=examplegroup)"      # 検索条件
    ldap.ldapserver = "ldap.example.net"       # LDAPサーバ名
    ldap.ldapbasedn = "dc=example,dc=net"      # 検索時の base DN
    ldap.ldapbinddn = ''                       # LDAPサーバ接続ユーザ名
    ldap.ldappasswd = ''                       # LDAPサーバ接続パスワード
    ldap.ldapgroupattr = 'uniqueMember'
    list._memberadaptor = ldap
```

### グループに登録されている学生（ただし、休学中を除く）をメンバーとする場合

休学中を除く全学生をメンバーとする場合に比べて、グループに登録されている学生（ただし、休学中を除く）をメンバーとする場合は、単純には検索式が書けないことが多くなります。

そのような場合は、以下のように python でフィルタ条件を指定すると対処できます。

```py:extend.py
from Mailman.LDAPMemberships import LDAPMemberships

def myfilter(dn, attrs):
    status = ''
    if attrs.has_key('myAccountStatus'):
        status = "".join(attrs['myAccountStatus'])
    return ( status == "absent" )

def extend(list):
    ldap = LDAPMemberships(list)
    ldap.ldapsearch = "(cn=examplegroup)"      # 検索条件
    ldap.ldapserver = "ldap.example.net"       # LDAPサーバ名
    ldap.ldapbasedn = "dc=example,dc=net"      # 検索時の base DN
    ldap.ldapbinddn = ''                       # LDAPサーバ接続ユーザ名
    ldap.ldappasswd = ''                       # LDAPサーバ接続パスワード
    ldap.ldapgroupattr = 'uniqueMember'
    ldap.ldapfilterfunction = myfilter
    list._memberadaptor = ldap
```

### メールアドレスが独自属性に格納されている場合

学生のメールアドレスが、例えば `myCanonicalizedMailAddress` 属性に格納されている場合は、以下のように設定します。無指定の場合は、`mail` 属性を使います。

```py:extend.py
from Mailman.LDAPMemberships import LDAPMemberships

def extend(list):
    ldap = LDAPMemberships(list)
    ldap.ldapsearch = "(employeeType=student)" # 検索条件
    ldap.ldapserver = "ldap.example.net"       # LDAPサーバ名
    ldap.ldapbasedn = "dc=example,dc=net"      # 検索時の base DN
    ldap.ldapbinddn = ''                       # LDAPサーバ接続ユーザ名
    ldap.ldappasswd = ''                       # LDAPサーバ接続パスワード
    ldap.ldapmailattr = 'myCanonicalizedMailAddress'
    list._memberadaptor = ldap
```

## 注釈

[^1]: [Postfix と LDAP を組み合わせる方法](http://www.postfix-jp.info/trans-2.3/jhtml/LDAP_README.html)を使って部局構成員向け同報アドレスを実現していた時期もあったのですが、2つの問題点がありました。第1は、配送時にエラーが発生した場合、かなり大量のエラーメールがメール発信者に送り返されてしまうので、メール発信者が困ってしまうという問題。第2は、同報アドレスが spam 送信に利用されないよう、適切なアクセス制御が必要になるという問題です。私の場合は、[組織内メールサーバから送信されたメールのみを配送する設定](http://qiita.com/tsuchm/items/44a76392ca94d28f1617)と組み合わせて、アクセス制御を実現しています。