I have recently upgraded the mailing list manager software for FOCUS mailing list to Mailman 2.1. I have been running Mailman 2.0 since its beta days 2 years ago, and it has been a great piece of software. FOCUS mailing list is quite light in traffic (average 70-100 posts a month across all lists), and Mailman with all its strength seems to be an over kill for this task. Nevertheless, I've also learnt a great deal of Python programming from its sources. Thanks to Barry Warsaw and all those who contribute to this project!
This article documents my quest to work around generating email aliases for Postfix whenever a mailing list is added ore removed.
Setting up aliases between Mailman and MTA has always been tricky. For every mailing list hosted, you have to create several email aliases manually in order to pipe the incoming emails to Mailman's scripts. For example, these aliases need to be appended to /etc/aliases (or /etc/postfix/aliases for Postfix) when a new mailing list mbf was added.
mbf: "|/home/mailman/mail/mailman post mbf"
mbf-admin: "|/home/mailman/mail/mailman admin mbf"
mbf-bounces: "|/home/mailman/mail/mailman bounces mbf"
mbf-confirm: "|/home/mailman/mail/mailman confirm mbf"
mbf-join: "|/home/mailman/mail/mailman join mbf"
mbf-leave: "|/home/mailman/mail/mailman leave mbf"
mbf-owner: "|/home/mailman/mail/mailman owner mbf"
mbf-request: "|/home/mailman/mail/mailman request mbf"
mbf-subscribe: "|/home/mailman/mail/mailman subscribe mbf"
mbf-unsubscribe: "|/home/mailman/mail/mailman unsubscribe mbf"
Mailman 2.1 comes with automatic configuration for Postfix by generating aliases, virtual domain and associated configuration files every time a list is added or removed. However, it is not that flexible for me as I run quite a few virtual domains on my mail server, and the domain list.focus-unsw.org is dedicated for mailing lists. Instead of playing around the aliases file, I want to parse the "To:" address to determine which mailing list it is delivering to. Here's my approach...
Grab the source and install Mailman as normal. That includes creating the mailman user, configuring, compiling and installing Mailman, setting up crontab, etc. However, don't touch any MTA integration yet.
I am aiming to dedicate domain list.focus-unsw.org for mailing lists - any emails sent to this domain is assuming to be reserved for Mailman to consume. It includes assigning an A record for list.focus-unsw.org.
For /etc/postfix/main.cf, make sure you have the following configuration.
recipient_delimiter = +
virtual_maps = hash:/etc/postfix/virtual, regexp:/etc/postfix/virtual_regexp
Virtual domain mapping file "/etc/postfix/virtual_regexp" contains regular expressions to rewrite all the emails sending to list.focus-unsw.org to go to the user mailman. For emails sending to email@example.com, I want to rewrite them to go to mailman+foo-bar. Append these to this file:
I am using a .forward to pipe all the emails sent to user mailman to Mailman's mail handling script. However, Mailman's script ($HOME/mail/mailman in 2.1) requires two arguments - a command and a list name. When Postfix is doing a local delivery, it will append Deliver-To: to the email header, detailing the exact recipient in the delivery. Therefore I wrote a Python script to sit between .forward and Mailman to parse the email, learn about the mailman command and list name from its Deliver-To: header, and then pipe the result to Mailman's script with correct parameters.
Here's the .forward file in Mailman's home directory, which is /home/mailman in this case.
filter.py is the Python script I wrote, which can be downloaded here. It basically reads the email from standard input, line by line, until it reaches the Deliver-To: header. It then parse the header to work out required arguments to call /home/mailman/mail/mailman. It then pipes everything it receives to Mailman's process. Here's the syntax highlighted version:
from cStringIO import StringIO
output = StringIO()
found = 0
re_search = re.compile(r'^Delivered-To:\s*mailman\+(\w+)(-(\w+))?@')
for line in sys.stdin:
if not found:
m = re_search.search(line)
if m is not None:
lst = m.group(1)
cmd = m.group(3)
if cmd is None:
cmd = 'post'
elif cmd not in [
'admin', 'bounces', 'confirm', 'join', 'leave', 'owner',
'request', 'subscribe', 'unsubscribe',
print >> sys.stderr, 'Invalid command "%s"' % cmd
wrapper = "/home/mailman/mail/mailman '%s' '%s'" % \
(cmd, lst.replace("'", "\\'"))
wrapper = os.popen(wrapper, "w")
except os.Error, ex:
print >> sys.stderr, 'Cannot execute mailman.', ex
output = wrapper
found = 1
elif line.strip() == '':
print >> sys.stderr, 'Deliver-To header cannot be found.'
if not found:
if output.close() is not None:
After it is done, there is no modification needs to be done to both Postfix or Mailman whenever new mailing lists are created, or abandoned lists are purged. It is probably less efficient because for every mailing list post delivered, two processes (mailman and filter.py) need to be started instead of just one. I have been running this script (with modifications every now and then) for the last two years without any problem. Originally it was actually written in Perl, but I have since decided that Python is a much better scripting language :)