My Little Corner of the Net

Using Mailman with Vesta on CentOS 6

About a year and a half ago I moved several of my websites to a server running the VestaCP control panel. At the time I posted an extensive review of the software, including a footnote that I found a way to get Mailman working with it. Apparently this is something a lot of people want to do because, since then, several people have contacted me looking for instructions. Life’s been hectic, and this wasn’t on the top of my mind, but I promised that I’d write a post. So, better late than never, here it is…

This tutorial will help you get GNU Mailman 2.1.x running on a Linux server. It’s geared toward CentOS 6.x, but will probably work with other Linux distros, although some file paths may change. It also assumes a standard VestaCP installation, with both Apache and Nginx running on the server.

Before we get too far into this, I should also point out that this tutorial only gets Mailman running on a Vesta server, it does not integrate it with the Vesta web interface. This means that you, as root, will need to set up new lists on the command line—you won’t be able to let your users create their own lists. Once a list is set up, however, it can be completely administered through the Mailman web interface, so hopefully this won’t be too big a deal for most situations.

Mailman is available as a CentOS package, but at the time that I did the install, several of the big email providers had recently made changes to their DMARC policies that broke older versions of Mailman, so I chose to build from the then-latest source release, which included new workarounds to address the issue.

When I did my installation, Mailman 3 was in beta, I believe, but since it wasn’t yet stable, and since I was moving existing Mailman 2.1 lists, I chose to stick with 2.1. Mailman 3 is now generally available and has some interesting new features, but it’s a significantly different beast and this tutorial probably won’t be helpful if you want to jump to 3.0. Fortunately, Mailman 2.1 is still in support and is still receiving regular updates, with 2.1.20 being the most recent version as of this writing.

This tutorial assumes that System V is used to manage services on the server. Many new Linux distros, including CentOS 7, have switched to systemd, which uses a different configuration format. If you’re using a systemd-based distribution, you’re on your own to figure that out, as I haven’t tried to do it yet (with Mailman anyway) myself.

There are four main parts to getting Mailman running on Vesta:

  • Install Prerequisites
  • Building and installing Mailman
  • Configuring Exim
  • Configuring Apache and Nginx
  • Creating your lists

While Mailman 2.1 supports multiple domain names, it does not allow the same list name to be used multiple times on different domains. In other words, if you host cats.com and dogs.com on the same server, and you create a customers@cats.com list, you can’t also create a customers@dogs.com list. While this wasn’t really a deal breaker for me, I decided to come up with a work around anyway, taking my lead from cPanel. cPanel appends the domain name onto the list name (i.e. customers_cats.com), but somehow strips it out in the email messages so that users only see customers@cats.com. I couldn’t figure out exactly how cPanel does this, but I came up with a pretty good facsimile by using Exim’s address rewriting features.

Installing Prerequisites

Mailman requires that the a C compiler and the Python development libraries be installed on the server, which are not installed by default. In addition, the pip command is required to install the dnspython Python module:

yum install -y gcc gcc-c++ python-devel python-pip

Now run the following command to install dnspython:

pip install dnspython

Building Mailman

We’ll start by downloading the latest release of the 2.1 branch of Mailman (2.1.20 as of this writing). Check http://launchpad.net/mailman to be sure you’re using the latest version.

wget https://launchpad.net/mailman/2.1/2.1.20/+download/mailman-2.1.20.tgz

Unzip the package

tar xvzf mailman-2.1.20.tgz

Switch to the directory that was created when the package was unzipped

cd ./mailman-2.1.20

Create the account and group that Mailman will run under:

useradd -r mailman

Mailman expects that the directories it will be installed into exist when you start the installation. Create those directories and set the ownership and permissions that Mailman requires:

mkdir -p /usr/local/mailman /var/mailman
chown mailman.mailman /usr/local/mailman /var/mailman
chmod 02775 /usr/local/mailman /var/mailman

Run the configure script to ensure all necessary libraries are available and to get Mailman ready to build:

./configure --prefix=/usr/local/mailman --with-var-prefix=/var/mailman --with-mail-gid=mailman --with-cgi-gid=apache

Build the package:

make

And install it:

make install

Run the check_perms script to ensure that permissions are as Mailman expects them to be:

/usr/local/mailman/bin/check_perms

If the above script returns any errors (and it probably will), run it again with the -f flag to have it try to fix the errors. In some cases, you may need to do this a few times before everything works.

/usr/local/mailman/bin/check_perms -f

Copy the /var/mailman/scripts/mailman file to /etc/init.d. This is the script that will be used to start and stop the mailman service:

cp /usr/local/mailman/scripts/mailman /etc/init.d

Copy the mailman crontab file to /etc/cron.d so that Mailman’s periodic tasks, such as sending out email reminders of posts awaiting moderation and managing the list archive, are run regularly.

cp /usr/local/mailman/cron/crontab.in /etc/cron.d/mailman

Now set a default password for Mailman. This password can be used in place of any list’s administrator password, so be sure to select a strong password.

/usr/local/mailman/bin/mmsitepass

Mailman requires that a default list, aptly named “mailman,” be created. You’ll be prompted for an administrator email address and a list password when you run this command. You can ignore the list of aliases that is displayed when you run the command.

/usr/local/mailman/bin/newlist mailman

Configure the system so that Mailman is automatically started when the server boots up:

chkconfig mailman on

And start the mailman service:

service mailman start

Mailman writes its log files to /var/mailman/logs. To be more consistent with other services, thereby making the logs easier to find, symlink Mailman’s logs directory in /var/log:

ln -s /var/mailman/logs /var/log/mailman

You’ll also want to rotate these logs on a regular basis so that they don’t get too big. To do this, create a new log rotate script. (Note: I prefer vi and use it in any instructions that require editing files in this tutorial, but feel free to substitute nano or your favorite editor if you don’t know vi or prefer something else.)

vi /etc/logrotate.d/mailman

Add the following contents to that file:

/var/log/mailman/bounce /var/log/mailman/digest /var/log/mailman/error /var/log/mailman/post /var/log/mailman/smtp /var/log/mailman/smtp-failure /var/log/mailman/qrunner /var/log/mailman/locks /var/log/mailman/fromusenet /var/log/mailman/subscribe /var/log/mailman/vette {
  missingok
  sharedscripts
  postrotate
  /usr/local/mailman/bin/mailmanctl reopen >/dev/null 2>&1 || true
  endscript
}

Configuring Exim

The next step in the configuration process is to integrate it with Exim, Vesta’s mail transfer agent (MTA). This allows Vesta to properly route incoming messages sent to a Mailman list to the Mailman software to process them. Exam is actually a preferred MTA to use with Mailman because it’s ability to route messages based on directory listings means that no per-list Exim configuration is necessary. By contrast, most other MTAs require you to set up several email aliases for each list you create.

The next several steps require editing the /etc/exim/exim.conf file. To make what’s going on more understandable, I’m going to start at the bottom of the file and work my way toward the top.

First, create a backup of the conf file, just to be safe:

cp /etc/exim/exim.conf /etc/exim/exim.bak

Then open it to edit.

vi /etc/exim/exim.conf

As mentioned above, Mailman does not support lists on multiple domains that share the same name. To work around this, I decided to follow cPanel’s lead and appended the domain name to the list name in the form listtname__domain.tld__. (Note that the trailing double underscore is necessary to properly parse address that contain a command, such as “-unsubscribe.” I couldn’t get the rewrite to properly parse these addresses without the underscores there.) This creates list email addresses that look like listname__domain.tld__@domain.tld which is undesireable. When mail is sent, however, Exim rewrites the email addresses it finds in the message into the preferred form, listtname@domain.tld, so end users see the cleaner form of the address.

Jump to the bottom of the file and find a line that starts with “begin rewrites” and add the following after that line:

#messages generated by Mailman will have the format of list__domain__@domain
#this rule will rewite them to list@domain before they are delivered
^([a-z0-9-\.]+)__[a-z0-9-\.]+__(-[a-z0-9]+)?@(.*) $1$2@$3 SEh

The above rewrite rule will strip the extraneous domain name from the list name when messages are sent, but only when the address appears in specific email headers. While this ensures that the to, from, and cc headers, for example are rewritten, it does not rewrite some of the more obscure headers, such as those that instruct mail clients how to handle unsubscribe requests. For this reason, we include two transports and two routers, the next features we’ll configure, that will properly route inbound messages that use either address format.

Transports that tell Exim how to handle a given incoming message. In this case, the transports instruct Exim to open a pipe to Mailman, when it receives a message associated with a list, through which it will pass the contents of the message, allowing Mailman to take over the processing.

Find the line “begin transports,” and add the following lines after this line but before the “begin rewrites” line.  There are several other transports already defined.  Where you put these doesn’t matter, as long as they’re in the “transports” section of the file.

mailman_transport:
  driver = pipe
  command = /usr/local/mailman/mail/mailman \
    '${if def:local_part_suffix \
    {${sg{$local_part_suffix}{-(\\w+)(\\+.*)?}{\$1}}} \
    {post}}' \
    ${lc:$local_part}__${lc:$domain}__
  current_directory = /usr/local/mailman
  home_directory = /usr/local/mailman
  user = mailman
  group = mailman
mailman_transport_norewrite:
  driver = pipe
  command = /usr/local/mailman/mail/mailman \
    '${if def:local_part_suffix \
    {${sg{$local_part_suffix}{-(\\w+)(\\+.*)?}{\$1}}} \
    {post}}' \
    ${lc:$local_part}
  current_directory = /usr/local/mailman
  home_directory = /usr/local/mailman
  user = mailman
  group = mailman

Finally, we create two routers. These tell Exim where to look to determine whether an incoming message has a valid destination on the server and, when a match is found, which transport it should be routed to for processing.

Add the following lines to the file between “begin routers” and “begin transports.”  Like with the transports, positioning doesn’t matter, as long as both definitions appear before the “begin transports line of the file.

mailman_router:
  driver = accept
  require_files = /usr/local/mailman/mail/mailman : \
    /var/mailman/lists/${lc::$local_part}__${lc::$domain}__/config.pck
  local_part_suffix_optional
  local_part_suffix = -admin : \
    -bounces : -bounces+* : \
    -confirm : -confirm+* : \
    -join : \
    -leave : \
    -owner : \
    -request : \
    -subscribe : \
    -unsubscribe
  transport = mailman_transport
mailman_router_norewrite:
  driver = accept
  require_files = /usr/local/mailman/mail/mailman : \
    /var/mailman/lists/${lc::$local_part}/config.pck
  local_part_suffix_optional
  local_part_suffix = -admin : \
    -bounces : -bounces+* : \
    -confirm : -confirm+* : \
    -join : \
    -leave : \
    -owner : \
    -request : \
    -subscribe : \
    -unsubscribe
  transport = mailman_transport_norewrite

Save the changes to the file and restart exim to enable them:

service exim restart

Configuring Apache

Configuring Apache was a bit of a challenge because Mailman is very specific about which user account it runs under. Vesta, on the other hand, uses suexec to run all scripts under the UID of the site owner, which breaks mailman. The solution is to run Mailman on a different port, where it is not bound by suexec’s rules. Later, we’ll set up a proxy in Nginx so that users won’t need to remember complicated URLs to manage their lists.

Mailman’s web-based admin tool has several small images at the bottom, which it expects to find in Apache’s /var/www/icons directory. Create symlinks in the icons directory that point at these images.

cp -s /usr/local/mailman/icons/* /var/www/icons

Create a new configuration file for Mailman’s Apache configuration.

vi /etc/httpd/conf.d/mailman.conf

Add a listen directive at the very top of this file. This tells Apache to bind to port 8090 when it starts and to listen for HTTP connections on this port.

Listen 8090

Next, add a VirtualHost block to handle requests coming in to this port.

<VirtualHost *:8090>
  ScriptAlias /mailman/ /usr/local/mailman/cgi-bin/
 
  AllowOverride None
  Options ExecCGI
  Order allow,deny
  Allow from all

  Alias /pipermail/ /usr/local/mailman/archives/public/
  Options Indexes MultiViews FollowSymLinks
  AllowOverride None
  Order allow,deny
  Allow from all
</VirtualHost>

Save the file and restart Apache:

service httpd restart

Now, depending on your firewall settings, you may be able to access the Mailman web interface at http://domain.tld:8090/mailman/listinfo. If you can’t, don’t worry about it as we’ll set that up next.

Configuring Nginx

Next, Nginx needs to be configured on each site to proxy requests to Mailman’s Apache listener on port 8090. Fortunately, we only need to apply this change to a few template files. Vesta offers a tool to reapply the template to a site’s configuration, which is a big help if you already have a lot of sites configured on the server.

Vesta stores it’s Nginx configuration templates in /usr/local/vesta/data/templates/web/nginx. This directory contains templates for each of the “proxy templates” you can choose when setting up a hosting package. Files with the extension .tpl are for HTTP configurations and .stpl files are for HTTPS configurations. You’ll need make the follow edits to each of the templates in the directory (or at least each of the templates that you use on your server).

We’ll start with HTTPS. Mailman, for the most part, uses relative URLs for most of it’s interface, so it runs fine on both HTTP and HTTPS. The user administration pages, however, use absolute URLs, so when managing users you can be dropped to HTTP unexpectedly. While not a standard module of Nginx, the CentOS builds include the ngx_http_sub_module, which can do string substitutions on page output. We can use this to rewrite the HTTP URLs in Mailman’s output to HTTPS to avoid problems.

Open the hosting.stpl file in the directory noted above:

vi /usr/local/vesta/data/templates/web/nginx/hosting.stpl

Between the end of the block that begins “location /error/“ and before the beginning of the block that starts “location @fallback” add the following:

location ~ ^/((mailman|pipermail)/?.*)$ {
  proxy_pass http://127.0.0.1:8090/$1$is_args$args;
  sub_filter http://%domain_idn% https://%domain_idn%;
  sub_filter_once off;
}
location /icons/ {
  alias /var/www/icons/;
}

Save the file and do the same for the other .stpl files in the directory.

There are two options for the HTTP configuration. If you know that all of your sites will have SSL certificates, as mine do, you can use the following configuration to direct all HTTP requests to their HTTPS counterparts. I recommend this approach if you can support it.

Open the hosting.tpl file:

vi /usr/local/vesta/data/templates/web/nginx/hosting.tpl

Again, between the “location /error/“ and “location @fallback” blocks, add the following:

location ~ ^/((mailman|pipermail)/?.*)$ {
  rewrite ^(.*)$ https://$host$1;
}
location /icons/ {
  alias /var/www/icons/;
}

And, like before, repeat the change on the other .tpl files as well.

If you can’t rely on all of your sites having SSL available, you can instead use a variation on the HTTPS configuration that doesn’t include the HTTP-to-HTTPS conversion. Follow the same instructions for the above HTTP changes, but add the following instead:

location ~ ^/((mailman|pipermail)/?.*)$ {
  proxy_pass http://127.0.0.1:8090/$1$is_args$args;
}
location /icons/ {
  alias /var/www/icons/;
}

Now that the templates are updated, we need to apply them to each existing site. To do this, run the following command for each hosting user account (not domain) on the server:

/usr/local/vesta/bin/v-rebuild-web-domains username

Once all accounts have been updated, restart Nginx to make the changes take effect:

service nginx restart

Creating Lists

Mailman is now up and running. All that’s left to do is to start creating lists.

When setting up a new list, remember that it is necessary to use the full address syntax, listname__domain.tld__@domain.tld for the list address. You should also specify both the emailhost and urlhost options to ensure that the list is configured correctly.

/usr/local/mailman/bin/newlist —emailhost=domain.tld —urlhost=www.domain.tld listname__domain.tld__@domain.tld

You’ll be prompted for an email address of the list administrator and for a list password. When the list is set up, you can access it at http://www.domain.tld/mailman/admin/listname__domain.tld__.

Since, through the Exim rewrites, we’re running the list from a different address than we configured Mailman to use, it is necessary to make one small settings change to the list’s settings. Open the list’s administrative page in a browser and log in with the password you provided in the previous step.

Once logged in, click on “Privacy Options” in the “Configuration Categories” menu. Then click on “Recipient Filters.”

On the Recipient Filters page, find the field labelled “Alias names (regexps) which qualify as explicit to or cc destination names for this list” and add the preferred list address (listtname@domain.tld). Without this, Mailman will not recognize the preferred address as being a valid list address and will hold any messages that are sent using it for moderation.

Click the “Submit Your Changes” button to save the change. Then make any additional settings changes you require and add recipients to the list under “Membership Management” in the “Configuration Categories” menu. You’re list is now ready to use.

6 Comments to Using Mailman with Vesta on CentOS 6

  1. Daniel's Gravatar Daniel
    12/18/2015 at 3:55 pm | Permalink

    Hi
    Thank you for the instructions. I’ve tried them on CentOS7 (saw your comment about systemd, but hoped you might lead me to the right direction)

    I could follow your precise instructions with those exceptions:

    ScriptAlias /mailman/ /usr/local/mailman/cgi-bin/
    # AllowOverride none
    Options ExecCGI
    # Order allow,deny
    # Allow from all

    Alias /pipermail/ /usr/local/mailman/archives/public/
    Options Indexes MultiViews FollowSymLinks
    # AllowOverride none
    # Order allow,deny
    # Allow from all

    I also hafd to add ‘bin’ for the rebuild:
    /usr/local/vesta/bin/v-rebuild-web-domains username

    Everything seems to run, but I cannot access the mailman interface on :8090
    I’ve added 8090 to allow in the firewall

    Could you give me a hint where to look for an error message?

    Tnx!

  2. 12/30/2015 at 3:36 pm | Permalink

    I have exactly the same problem with Daniel (running Ubuntu 14.04). Getting error that “allow is not allowed in here”. Any ideas?

  3. casdr's Gravatar casdr
    01/01/2016 at 8:06 am | Permalink

    Hi,

    I’m getting the following error from exim:
    2016-01-01 07:59:50 H=localhost ([ip]) [::1] F= rejected RCPT : relay not permitted
    2016-01-01 08:01:45 H=localhost ([ip]) [::1] sender verify defer for : remote host address is the local host

    Do you know how to fix this?

Leave a Reply

You can use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

<