README.postfix Jan 28th 2012
The main challenge to setting up Mlmmj with Postfix is that Mlmmj must be
executed by root or the owner of the list directory, but by default Postfix
will execute Mlmmj as 'nobody'.
There are a number of possible ways around this:
- Making 'nobody' own your lists (insecure) 
- Changing the Postfix default to an 'mlmmj' user (possibly insecure or
- .forward files (impractical) 
- Using an :include: file owned by an 'mlmmj' user (possibly insecure and
- Adding an alias table owned by an 'mlmmj' user (suboptimal) 
- Using a Postfix transport to run Mlmmj as an 'mlmmj' user (recommended)
As you can see, the last option is recommended. Here is how to set it up using
Postfix virtual domains (so you can host multiple domains on the same server).
(It can also be done with regular non-virtual aliases.)
1) Add an 'mlmmj' user to your system (e.g. using 'useradd'). It usually
makes sense to make this a 'system' user, with no password and no shell
(/usr/false for the shell), and for its home directory to be
/var/spool/mlmmj (or wherever you want to put your Mlmmj spool directory).
2) Create your Mlmmj spool directory (we'll assume it's /var/spool/mlmmj)
and change its owner to the 'mlmmj' user.
3) Add an 'mlmmj' transport which uses the pipe(8) delivery agent to execute
mlmmj-receive as the mlmmj user by adding something like the following to
master.cf (often in /etc/postfix):
# mlmmj mailing lists
mlmmj unix - n n - - pipe
flags=ORhu user=mlmmj argv=/usr/local/bin/mlmmj-receive -F -L /var/spool/mlmmj/$nexthop
Note that $nexthop is used to specify the list directory. We will return
to that later.
4) Integrate some necessary options in main.cf (also often in /etc/postfix):
# Only deliver one message to Mlmmj at a time
mlmmj_destination_recipient_limit = 1
# Consider the part after '+' but before '@' to be an address extension
# i.e. addresses have the form firstname.lastname@example.org
recipient_delimiter = +
# A map to forward mail to a dummy domain
virtual_alias_maps = hash:/var/spool/mlmmj/virtual
# Allow virtual alias maps to specify only the user part of the address
# and have the +extension part preserved when forwarding, so that
# list-name+subscribe, list-name+confsub012345678, etc. will all work
propagate_unmatched_extensions = virtual
# A map to forward mail for the dummy domain to the Mlmmj transport
transport_maps = hash:/var/spool/mlmmj/transport
Of course, you may need to merge these options with existing ones (e.g.
you probably have existing virtual_alias_maps if you run a multi-domain
It is probably unnecessary to change propagate_unmatched_extensions because
it defaults to something including 'virtual'. You can check this with
something like 'postconf | grep propagate'.
5) (For each list) Create a mailing list (e.g. by using mlmmj-make-ml). The
list directory should be like /var/spool/mlmmj/list-dir for a flat
structure, or /var/spool/mlmmj/domain.tld/list-name for a hierarchical
structure (the -s option to mlmmj-make-ml may be useful to get the list
created where you want it). Ensure the list directory and everything in it
is owned by the mlmmj user (except you may want control files to be owned
by your www server user in order to use web configuration interfaces; they
must be readable by the mlmmj user though).
6) (For each list) Add entries to the Postfix tables to accept mail for the
list and forward it to the Mlmmj transport:
# for a flat structure
# for a hierarchical structure
Note that we have used a dummy domain 'localhost.mlmmj' to connect the
virtual alias with the Mlmmj transport. This could be anything as long as
it isn't a real domain. The user part of the address could also be
anything; as long as the address matches in both tables it should work.
Also note that the text after 'mlmmj:' becomes $nexthop which was mentioned
earlier, so it is used to specify the list directory when executing
7) Refresh your postfix tables and reload your configuration so it takes
Enjoy your new lists!
 Actually, the standard local(8) delivery agent will execute external
programs (such as Mlmmj) as the 'receiving user'. However, unless you
direct your mail to Mlmmj using a .forward file (see local(8)) or an
:include: file (see aliases(5)), or your aliases file is not owned by root,
there is no 'receiving user'. Without a 'receiving user', Postfix uses the
user from the configuration option 'default_privs', which defaults to
 Making 'nobody' own your lists is insecure because other programs and
daemons rely on 'nobody' not owning any files or having access to anything;
they use 'nobody' as a way of denying access and keeping all your files and
system secure. Most notably, some NFS implementations use 'nobody' when
somebody connects but fails to authenticate. Your mailing lists should not
be accessible in such situations, but they may be if they are owned by
 Changing 'default_privs' to an 'mlmmj' user may open other security holes,
and may not be appropriate if Postfix is used for other external programs
 Using .forward files is not practical, as it requires a user to be created
for every mailing list.
 Using :include: files would require delivery to commands to be enabled in
:include: files, which is not recommended for security reasons. It is also
messy for virtual domains in the same way as an alias table owned by an
'mlmmj' user is.
 Adding an alias table owned by an 'mlmmj' user works, and doesn't pose any
great security risk. However, it is messy for virtual domains as you need
to forward mail from the virtual domain to your non-virtual domain and then
to Mlmmj. This results in each list having an additional address, which is
not desirable. That extra intermediate address is also included in mail
headers, which is not desirable (though it could be filtered out by Mlmmj).
Setting up an Mlmmj transport is about the same amount of work and doesn't
have these drawbacks. However, If you are not using virtual domains, this
is a good and simple option; but it will not be explained in detail here.
 To use non-virtual alises, at step 4, you'll need to incorporate:
alias_maps = hash:/var/spool/mlmmj/aliases
propagate_unmatched_extensions = alias
You probably will need to adjust propagate_unmatched_extensions in this
case, probably by adding 'alias' to the existing value rather than using
If you want to use 'newaliases' to update the alias table, you should also
alias_database = hash:/var/spool/mlmmj/aliases
At step 6, entries in /var/spool/mlmmj/aliases should look something like:
At step 7, you'll need:
or (if you included alias_database above)
And of course you can omit the virtual stuff if you're not using it.
Note that this has not been tested, but we believe it should work.
 The flags for the transport are pretty critical. In particular if the 'R'
option is not used mlmmj-receive fails to receive the mail correctly. The
D - Prepend a 'Delivered-To: recipient' header (not used)
O - Prepend an 'X-Original-To: recipient' header
R - Prepend a 'Return-Path:'. header
h - fold $nexthop to lowercase
u - fold $recipient to lowercase