Last modified: Monday, September 09, 2024

GNU Mailman3インストールメモ(FreeBSD+sendmail環境)

Mailman3, FreeBSD, sendmail, apache, postorius

FreeBSD Daemon is buried under a lot of mail. This image is generated by DALL-E.

Image generated by DALL-E

はじめに

極々身内だけの小規模なメーリングリストをこれまでGNU Mailman2で運用してきたのですが、 python2系もmailman2もEoLということで、渋々mailman3に移行しました。 有名どころでもあるのでそれほど苦も無く入るかと思ったのですが、 非っ常に面倒くさかったので忘れないうちにメモとして残しておきます。

誤解されないように書いておくと、 Linux+Postfixな環境だと多分それほど問題無く入るように思います。 参考にできるページもGoogleでよくヒットしますし、 パッケージでサクッと入るんではないかと。 ところがFreeBSDになると参考資料激減、 加えてsendmailとなるとほとんど見当たらないという状況でした。 途中、諦めてsympaに乗り換えようかと思ったのですが、 あっちもpostfix前提のような感じ(portsでは)だったので、やめました。


インストールした環境

mailman3のインストール・初期設定

必要なパッケージ(ports)はインストール済みとします。

いくつか検索でヒットしたページの中で、最終的に最も参考になったページが次の2つです。

両者の内容のマージみたいな形で設定しました。

利用しているmcファイルのMAILER_DEFINITIONSに以下を追加。


Mmm3lmtp,       P=[IPC], F=PSXmnz9, S=EnvFromSMTP/HdrFromSMTP,
                R=EnvToMM3, E=\r\n, L=1024,
                A=TCP $h 8024

同じくmcファイルのLOCAL_RULESETSに以下を追加。 (2行目のR$+と$:、3行目の$*と$:の間はタブで区切る)


SEnvToMM3
R$+     $: $>EnvToSMTP $1
R$+ < @ example . com . private . > $*       $: $1 < @ example . com . > $2

/etc/mail/mailertable に以下を追加。


example.com.private        mm3lmtp:[localhost]

/usr/local/lib/python3.9/site-packages/mailman/mta/sendmail.py を以下の内容で作成。



# Copyright (C) 2001-2022 by the Free Software Foundation, Inc.
#
# This file is part of GNU Mailman.
#
# GNU Mailman is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# GNU Mailman.  If not, see <https://www.gnu.org/licenses/>.

"""Creation/deletion hooks for the Sendmail MTA."""

import os

from collections import defaultdict
from contextlib import contextmanager
from flufl.lock import Lock
from mailman.config import config
from mailman.config.config import external_configuration
from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.mta import (
    IMailTransportAgentAliases, IMailTransportAgentLifecycle)
from mailman.utilities.datetime import now
from operator import attrgetter
from public import public
from zope.component import getUtility
from zope.interface import implementer


ALIASTMPL = '{0}	%1%3@{2}.private'



@contextmanager
def atomic(path):
    # Write a new file and then atomically rename it.
    new_path = path + '.new'
    try:
        with open(new_path, 'w', encoding='utf-8') as fp:
            yield fp
    except:                                      # noqa: E722 pragma: nocover
        os.remove(new_path)
        raise
    else:
        os.rename(new_path, path)
        os.chmod(path, 0o664)


def _get_alias_domain(domain):
    domain_manager = getUtility(IDomainManager)
    d = domain_manager.get(domain)
    if d is not None and d.alias_domain:
        return d.alias_domain
    return domain


class _FakeList:
    """Duck-typed list for the `IMailTransportAgentAliases` interface."""

    def __init__(self, list_name, mail_host):
        self.list_name = list_name
        self.true_mail_host = mail_host
        self.mail_host = _get_alias_domain(mail_host)
        self.posting_address = '{}@{}'.format(list_name, self.mail_host)


@public
@implementer(IMailTransportAgentLifecycle)
class LMTP:
    """Connect Mailman to Sendmail via LMTP."""

    def __init__(self):
        # Locate and read the Postfix specific configuration file.
        mta_config = external_configuration(config.mta.configuration)

    def create(self, mlist):
        """See `IMailTransportAgentLifecycle`."""
        # We can ignore the mlist argument because for LMTP delivery, we just
        # generate the entire file every time.
        self.regenerate()

    delete = create

    def regenerate(self, directory=None):
        """See `IMailTransportAgentLifecycle`."""
        # Acquire a lock file to prevent other processes from racing us here.
        if directory is None:
            directory = config.DATA_DIR
        lock_file = os.path.join(config.LOCK_DIR, 'mta')
        with Lock(lock_file):
            lmtp_path = os.path.join(directory, 'sendmail_virtusertable')
            with atomic(lmtp_path) as fp:
                self._generate_lmtp_file(fp)

    def _generate_lmtp_file(self, fp):
        # The format for Postfix's LMTP transport map is defined here:
        # http://www.postfix.org/transport.5.html
        #
        # Sort all existing mailing list names first by domain, then by
        # local part.  For Postfix we need a dummy entry for the domain.
        list_manager = getUtility(IListManager)
        utility = getUtility(IMailTransportAgentAliases)
        by_domain = {}
        sort_key = attrgetter('list_name')
        for list_name, mail_host in list_manager.name_components:
            mlist = _FakeList(list_name, mail_host)
            by_domain.setdefault(mlist.mail_host, []).append(mlist)
        print("""\
# AUTOMATICALLY GENERATED BY MAILMAN ON {}
#
# This file is generated by Mailman.  YOU SHOULD NOT MANUALLY EDIT THIS
# FILE unless you know what you're doing, and can keep the two files properly
# in sync.  If you screw it up, you're on your own.
""".format(now().replace(microsecond=0)), file=fp)
        for domain in sorted(by_domain):
            print("""\
# Aliases which are visible only in the @{} domain.""".format(domain),
                  file=fp)
            for mlist in sorted(by_domain[domain], key=sort_key):
                aliases = list(utility.aliases(mlist))
                # width = max(len(alias) for alias in aliases) + \
                #    aliases[0].count('.') + 10
                print(ALIASTMPL.format(aliases.pop(0),
                                       config, domain), file=fp)
                for alias in aliases:
                    print(ALIASTMPL.format(alias, config, domain), file=fp)
                print(file=fp)
            #print(f"@{domain}	error:5.1.1:550 User unknown", file=fp)

/usr/local/lib/python3.9/site-packages/mailman/config/sendmail.cfgとして以下のファイルを作成。


[sendmail]
# PLB started with exim4.cfg
# Additional configuration variables for sendmail

# sendmail doesn't need any additional configuration yet.

/usr/local/mailman/etc/mailman.cfgに以下を追加。


[mta]
incoming: mailman.mta.sendmail.LMTP
outgoing: mailman.mta.deliver.deliver
lmtp_host: 127.0.0.1
lmtp_port: 8024
smtp_host: localhost
smtp_port: 25
configuration: python:mailman.config.sendmail

これで /usr/local/mailman/data/sendmail_virtusertable に以下のような内容のファイルが生成されるようになる。 このファイルを /etc/mail/virtusertable として参照する。


mlname@example.com	%1%3@example.com.private
mlname-bounces@example.com	%1%3@example.com.private
mlname-confirm@example.com	%1%3@example.com.private
mlname-join@example.com	%1%3@example.com.private
mlname-leave@example.com	%1%3@example.com.private
mlname-owner@example.com	%1%3@example.com.private
mlname-request@example.com	%1%3@example.com.private
mlname-subscribe@example.com	%1%3@example.com.private
mlname-unsubscribe@example.com	%1%3@example.com.private

以上がsendmail対応の大筋になります。 多分、ここに書いた以外のところでエラーがたくさん出ると思いますが、 エラーメッセージに従って直していくことになります(たくさんありすぎて憶えていない…)


Postoriusのインストール・初期設定

こちらの作業の方がさらに面倒でした。 最も参考になった情報は以下のものです。

基本はこの手順に従って(自分のサイトに合わせて適宜書き換えながら)進めるだけですが、 ここでも不足モジュールのエラー等が多発するので言われた通りに対処していきます。 言語やタイムゾーンの設定は要修正かと思います。

あとはapacheの設定ファイル。mod_wsgiモジュールをLoadModuleして、 以下の設定を/usr/local/etc/apache24/Includes/ 以下に置きます。


Alias /static /usr/local/mailman/static
<Directory "/usr/local/mailman/static/">
    Require ip 適切なIPアドレス
</Directory>

WSGIScriptAlias /mailman3 /usr/local/mailman/wsgi.py process-group=mailman-web
WSGIScriptAlias /postorius /usr/local/mailman/wsgi.py process-group=mailman-web

WSGIPythonHome /usr/local
WSGIPythonPath /usr/local/mailman
WSGIDaemonProcess mailman-web display-name=mailman-web maximum-requests=1000 umask=0002 user=mailman group=mailman python-path=/usr/local/mailman home=/usr/local

<Directory "/usr/local/mailman">
	<Files wsgi.py>
		Require ip 適切なIPアドレス
	</Files>
	WSGIProcessGroup mailman-web
</Directory>

エラーが出るたびに設定ファイルを修正しますが、 uwsgiやapache, mailmanなどのサービスを再起動しないと変更が有効にならないときがありました(restartでは不十分で、stop & startが必要)。

おわりに

以上、FreeBSD+Sendmail+Apacheな環境でMailman3+Postoriusを動かすための情報でした。 もう次に同じようにインストールできる自信が無いですね。 とにかくパッケージとモジュールの依存が多すぎて、大したことを求めているわけでもないのに明らかにオーバスペックです。 大昔の majordomo は分かりやすかったのに、どうしてこうなったのか… 次はmlmmjに乗り換えるかもしれません。


追記(2024/7/12)

portsで提供されるpythonのデフォルトバージョンが3.9から3.11に上がったので、 いつもの手順で3.11に上げたところ、予想通りPostoriusが使えなくなってました。 取りあえず、apacheのエラーログを見ながら、pip install django-q してみたり、/usr/local/lib/python3.9/site-packages/mailman/config/sendmail.cfg や /usr/local/lib/python3.9/site-packages/mailman/mta/sendmail.py を /usr/local/lib/python3.11/以下の同等の場所に移したり、 /usr/local/etc/rc.d/apache24 と /usr/local/etc/rc.d/uwsgi を restart したり stop & start したりしているうちに Postorius にログインできるように戻りました。


布目 淳@京都工芸繊維大学コンピュータシステム研究室 (nunome@kit.ac.jp)