I've been migrating a set of Mailman mailing lists to new
servers recently, and have implemented an extension to the
standard exim+Mailman configuration which may be of
interest to others. Comments welcome, of course.
This trick is suitable for lists where only members (and
perhaps a small set of known nonmembers or alternate
addresses for members) are permitted posters, and
prohibited messages are rejected outright rather than
being moderated later. (Many lists which start out with
another policy end up having this policy de facto, because
of the hassle of sorting a few legitimate posts from other
rubbish in Mailman's moderation queue.) Using a small
Python script to check the configuration of a destination
list, it is possible to refuse mail from non-permitted
senders before it even reaches Mailman.
Unfortunately (and as with doing anti-spam testing at SMTP
time) it is necessary to restrict each message transaction
to exactly one list recipient. This is because permission
to post to a mailing list is based on the From: header
(and perhaps also Reply-To:), rather than the envelope
sender. Obviously these values are known only after the
DATA command.
The necessary script is here:
http://tea.ukcod.org.uk/~chris/tmp/20060703/mailman_test_sender
-- it calls into Mailman's own code, and may have to be
adapted for your local installation of Mailman (we use the
Debian packages which make an ambitious if ultimately
futile effort to contort Mailman's own arrangement of
files into something resembling the FHS). Because Mailman
uses on-disk files rather than a proper database, the
script needs to be able to read those files; in principle
one could do this with group membership, but (a) this made
me feel slightly uneasy security-wise; and (b) exim drops
supplementary group memberships so you'd have to have
Mailman run in exim's group rather than vice versa.
Therefore I instead implemented a restricted setuid
wrapper program, the source for which is here:
https://secure.mysociety.org/cvstrac/getfile/mysociety/utils/mailman_wrapper.c
-- you could instead achieve the same effect with sudo or
userv or whatever, if that's what floats your boat. That
wrapper also permits the execution of another script,
http://tea.ukcod.org.uk/~chris/tmp/20060703/mailman_list_domain
which, given the local-part of a list, returns its
corresponding domain name, which is useful if you have
Mailman lists in several different domains and don't want
them to clash with other sorts of names.
The relevant ACL configuration looks something like this:
(this is excerpted from our real mail config so is
probably not minimal)
- Firstly, set up some macros and a domain list (some of
these are in the standard howto):
domainlist mailman_domains = ...
MM_LISTCHECK = /var/lib/mailman/lists/${lc:$local_part}/config.pck
MM_QUERY = /etc/exim4/scripts/mailman_wrapper
# these are arbitrary
MAILMAN_LIST = acl_m1
MAILMAN_DOMAIN = acl_m2
- Then, in the RCPT ACL, enforce the `one list
delivery per transaction' constraint, and record some
information about the list for later:
# Special case for list mail. We want to ensure that each message is
# submitted to either exactly one mailing list, or any number of
# non-mailing-list addresses per submission, so that we can check that the
# sender is permitted at the end of the DATA statement.
defer domains = +mailman_domains
condition = ${if exists{MM_LISTCHECK} \
{${if >{${eval:$rcpt_count \
- $rcpt_defer_count \
- $rcpt_fail_count}}{1} \
{yes} {no} }} \
{no} }
message = Please make each Mailman delivery in a separate submission
# Record information about a list delivery
warn domains = +mailman_domains
condition = ${if exists{MM_LISTCHECK} }
set MAILMAN_LIST = ${lc:$local_part}
set MAILMAN_DOMAIN = ${lc:$domain}
message = destination is Mailman list ${lc:$local_part}
- Finally, in the DATA ACL, call out to the script to
test whether a given list delivery is permitted:
# If this message is to a Mailman list, check that the sender is permitted to
# post to the list. This is slightly nasty, because Mailman (correctly) uses
# the From: header to decide whether the sender is authorised, which is why
# we need to do this test in the DATA ACL rather than at recipient time.
deny message = You are not permitted to send mail to the $MAILMAN_LIST \
mailing list. Please contact \
$MAILMAN_LIST-owner@$MAILMAN_DOMAIN for further \
information.
# Mailman considers the "sender" of a mail to be the From: address,
# envelope sender, Reply-To: address or Sender: address. For our
# purposes the order does not matter.
condition = ${if !eq{$MAILMAN_LIST}{} \
{${run {MM_QUERY test-sender $MAILMAN_LIST \
$sender_address \
${address:$h_From:} \
${address:$h_Reply-To:} \
} {no} {yes} }} \
{no} }
A couple of issues with this: firstly, the test-sender
script doesn't implement Mailman's full logic for
accepting/denying/queueing an incoming mail. It could be
extended to do so, though reimplementing it in the way I
do about is a bit ugly. Secondly, forking and execing the
script is going to be slow, but in most installations this
probably won't matter (if it does then write a daemon
which responds to queries on some socket; that would also
get around the irritating privilege-boundary issue).
More generally this has got me wondering about how much of
a mailing-list manager could be implemented within a
modern MTA. It is painfully obvious that most of Mailman's
queueing and aliasing stuff could equally well be
implemented with Exim and a database and suitable
transports. More difficult is what to do with the
moderation queue aspect of the problem, which is required
in some mailing lists.
One evil possibility would be to defer mail in an ACL,
while recording a copy in the moderation queue; the result
of moderation could then be used to decide whether to
accept or decline the message the next time the sending
MTA submits it (identifying the message by its Message-ID:
or perhaps a loose hash of its contents). That has some
bad properties (the message might never be resubmitted;
there's a delay between moderator approval and the message
reaching the list). Alternatively on approval the mail
could be passed to the list, and the next time the sending
MTA resubmits it, it could be accepted and silently
dropped, having already reached its recipients; of course,
that has the converse bad property that if it's never
resubmitted the sender will have received a misleading
error report. Perhaps this is just one of those things
which can't quite be sensibly done at SMTP time.
--
``Fill your tank with 800 gallons of water. Start yesterday. Remember, a
medium-sized hippo takes up at least 200 gallons. (Just out of curiosity,
why do you have a hippo, anyway?)'' (USPS, how to move a hippo)