Using Mailman 2 in a machine without a mail server
Mailman is a mailing list software written in Python. It includes mailing list operation, management and a web interface.
Advertencia
The content of this article refers to Mailman 2. This won't probably work AS IS in Mailman 3. I haven't migrated yet.
Usually Mailman is running in a machine having a web server and a mail server. In this article I will describe how to operate a Mailman environment where the mail server resides in a different machine. Why? Because I already have a mail server running. Taking care of configuration, operation and spam filtering of an additional service is a burden.
There are two interfaces between Mailman 2 and the mail server:
-
Outgoing: Fortunately you can configure the mail server used for outgoing emails editing the configuration file mm_cfg.py and modifying the variable SMTPHOST.
Problem solved.
-
Incoming: There is a small script from Mailman 2 that the mail server runs for each email received. This script injects the email in a Mailman 2 queue to be processed by the Mailman machinery.
We need to replace that script to transport the emails to the Mailman 2 incoming queue running in a different machine.
For Postfix (my mail server) you usually have something like this in your aliases file:
hacking: "|/home/mailman/mail/mailman post hacking" hacking-admin: "|/home/mailman/mail/mailman admin hacking" hacking-bounces: "|/home/mailman/mail/mailman bounces hacking" hacking-confirm: "|/home/mailman/mail/mailman confirm hacking" hacking-join: "|/home/mailman/mail/mailman join hacking" hacking-leave: "|/home/mailman/mail/mailman leave hacking" hacking-owner: "|/home/mailman/mail/mailman owner hacking" hacking-request: "|/home/mailman/mail/mailman request hacking" hacking-subscribe: "|/home/mailman/mail/mailman subscribe hacking" hacking-unsubscribe: "|/home/mailman/mail/mailman unsubscribe hacking"
This configuration will run /home/mailman/mail/mailman script (my Mailman 2 is installed in /home/mailman/) when a mail is received in those email addresses. This requires that Mailman 2 and Postfix are running in the same machine.
I am going to change that. I will use Pyro, a Python remote procedure call library. Why? Because it is easy to use, it requires no infraestructure and it is mostly Python transparent.
This is the code I use in the machine running my Postfix:
This code will be executed by Postfix when a new email arrives. Execution starts at line 40. The only thing we do is to call main(). If anything goes wrong we print some details and we indicate Postfix to try again soon (line 52). If everything was fine, we give to Postfix the return code returned by the remote enqueueing process (line 57).
The heart is routine main() (lines 15-38). Here we import Pyro (line 16) inside the routine because we want an import failure (Pyro not installed, for example) to be a temporal failure to be notified, not a catastrofic mail loss.
Interestingly, default Pyro serializer makes transporting binary data non trivial, so I use an alternative builtin marshal serializer (line 21). We indicate where the service is located (line 23) and the connection timeout (line 24).
Next step is to get the details of the Postfix request (line 26) and the content of the email (line 31).
Then we do the remote procedure call (line 33). If everything is fine, we send to stdout and stderr the content the remote enqueueing process generated and we return to the caller the return code. The idea here is that the RPC should be transparent and the sysadmin should feel that the enqueueing behaves and provides the same diagnostics that a local version.
This code will reside in /home/postfix/z-mailman.py.
Now we change Postfix aliases configuration file to this:
hacking: "|/home/postfix/z-mailman.py post hacking" hacking-admin: "|/home/postfix/z-mailman.py admin hacking" hacking-bounces: "|/home/postfix/z-mailman.py bounces hacking" hacking-confirm: "|/home/postfix/z-mailman.py confirm hacking" hacking-join: "|/home/postfix/z-mailman.py join hacking" hacking-leave: "|/home/postfix/z-mailman.py leave hacking" hacking-owner: "|/home/postfix/z-mailman.py owner hacking" hacking-request: "|/home/postfix/z-mailman.py request hacking" hacking-subscribe: "|/home/postfix/z-mailman.py subscribe hacking" hacking-unsubscribe: "|/home/postfix/z-mailman.py unsubscribe hacking"
In the machine we have Mailman 2 we need to run the other end of the Pyro remote procedure call. The code is quite simple too:
Code execution starts at line 35. We bind the network details (line 35), register the service we export (line 36) and just sit there processing requests (line 38).
The requests are processed in class mailman (lines 13-25). We basically just call the regular Mailman 2 enqueueing script (lines 18-22). Note the use of a timeout and that timeout is shorter than the request timeout used in the Postfix machine. Note also how we return stdout and stderr to the calling process. We are careful about transfering binary data.
Lines 27-32 are interesting because we are implementing an access control based on IP address. We don't want anybody to be able to inject arbitrary emails in our mailing lists. We could use a shared secret, but since the data travels in the open, it is pointless. [1]
This code in the Mailman 2 machine must be always running. I am using a SmartOS native zone (a Solaris derivative), so I declare a SMF manifest:
To avoid that anybody running in the local machine can use Mailman 2 enqueueing script to inject arbitrary emails in the mailing lists, Mailman 2 enqueueing script verifies the username and group of the calling process. In my installation I have configured Mailman 2 to require an username and group of nobody.
I also disable Python buffering (line 55) and instruct Python to use UTF-8 (line 54) for Input/Output independently of the Operating System default encoding.
After importing this manifest, the Operating System will take care of launching the service at boot time, restarting it if it dies, logging, etc.:
[root@babylon5 home]# svcs mailman-Pyro4 STATE STIME FMRI online Oct_28 svc:/jcea/mailman-Pyro4:default [root@babylon5 home]# svcs -l mailman-Pyro4 fmri svc:/jcea/mailman-Pyro4:default name Pasarela Pyro4 a Mailman enabled true state online next_state none state_time Sat Oct 29 19:22:12 2016 logfile /var/svc/log/jcea-mailman-Pyro4:default.log restarter svc:/system/svc/restarter:default contract_id 142 dependency require_all/error svc:/network/loopback:default (online) dependency optional_all/error svc:/network/physical:default (online) dependency require_all/none svc:/system/filesystem/local (online)
[1] | Update 20171123: Pyro 4.62 added support for TLS. Your traffic can be encrypted and you can use X.509 certificates for mutual authentication. |