summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/config/postfix.cfg5
-rw-r--r--src/mailman/config/schema.cfg7
-rw-r--r--src/mailman/docs/mta.rst41
-rw-r--r--src/mailman/mta/postfix.py49
-rw-r--r--src/mailman/mta/tests/test_aliases.py168
-rw-r--r--src/mailman/testing/layers.py1
6 files changed, 203 insertions, 68 deletions
diff --git a/src/mailman/config/postfix.cfg b/src/mailman/config/postfix.cfg
index 9bdba221e..cddda220a 100644
--- a/src/mailman/config/postfix.cfg
+++ b/src/mailman/config/postfix.cfg
@@ -6,3 +6,8 @@
# be appended to this string (with a separating space), so it must be
# appropriate for os.system().
postmap_command: /usr/sbin/postmap
+
+# This variable describes the type of transport maps that will be generated by
+# mailman to be used with postfix for LMTP transport. By default, it is set to
+# hash, but mailman also supports `regex` tables.
+transport_file_type: hash
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index fde431b60..9568694be 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -563,9 +563,12 @@ enabled: yes
# This value contains lines which specify RFC 822 headers in the email to
# check for spamminess. Each line contains a `key: value` pair, where the key
# is the header to check and the value is a Python regular expression to match
-# against the header's value. E.g.:
+# against the header's value. Multiple checks should be entered as multiline
+# value with leading spaces:
#
-# X-Spam: (yes|maybe)
+# header_checks:
+# X-Spam: (yes|maybe)
+# Authentication-Results: mail.example.com; dmarc=(fail|quarantine)
#
# The header value and regular expression are always matched
# case-insensitively.
diff --git a/src/mailman/docs/mta.rst b/src/mailman/docs/mta.rst
index 1d2bbc268..54095bc68 100644
--- a/src/mailman/docs/mta.rst
+++ b/src/mailman/docs/mta.rst
@@ -145,9 +145,12 @@ Transport maps
By default, Mailman works well with Postfix transport maps as a way to deliver
incoming messages to Mailman's LMTP server. Mailman will automatically write
the correct transport map when its ``mailman aliases`` command is run, or
-whenever a mailing list is created or removed via other commands. To connect
-Postfix to Mailman's LMTP server, add the following to Postfix's ``main.cf``
-file::
+whenever a mailing list is created or removed via other commands. Mailman
+supports two type of transport map tables for Postfix, namely ``hash`` and
+``regexp``. Tables using hash are processed by ``postmap`` command. To use this
+format, you should have ``postmap`` command available on the host running
+Mailman. It is also the default one of the two. To connect Postfix to
+Mailman's LMTP server, add the following to Postfix's ``main.cf`` file::
transport_maps =
hash:/path-to-mailman/var/data/postfix_lmtp
@@ -167,6 +170,36 @@ strictly needed (but it is harmless). All you need to do in this scenario is
to make sure that Postfix accepts mail for your one domain, normally by
including it in ``mydestination``.
+Regular Expression tables remove the additional dependency of having ``postmap``
+command available to Mailman. If you want to use ``regexp`` or Regular
+Expression tables, then add the following to Postfix's ``main.cf`` file::
+
+ transport_maps =
+ regexp:/path-to-mailman/var/data/postfix_lmtp
+ local_recipient_maps =
+ regexp:/path-to-mailman/var/data/postfix_lmtp
+ relay_domains =
+ regexp:/path-to-mailman/var/data/postfix_domains
+
+You will also have to instruct Mailman to generate regexp tables instead of hash
+tables by adding the following configuration to ``mailman.cfg``::
+
+ [mta]
+ incoming: mailman.mta.postfix.LMTP
+ outgoing: mailman.mta.deliver.deliver
+ lmtp_host: mail.example.com
+ lmtp_port: 8024
+ smtp_host: mail.example.com
+ smtp_port: 25
+ configuration: /path/to/postfix-mailman.cfg
+
+Also you will have to create another configuration file called as
+``postfix-mailman.cfg`` and add its path to the ``configuration`` parameter
+above. The ``postfix-mailman.cfg`` would look like this::
+
+ [postfix]
+ transport_file_type: regex
+
Postfix documentation
---------------------
@@ -339,7 +372,7 @@ the user ``mailman``, qmail will give you the destination address
``mailman-spam@lists.example.com`` while it should actually be
``spam@lists.example.com``. The second argument to ``qmail-lmtp`` defines
how many parts (separated by dashes) to filter out. The first argument
-specifies the LMTP port of mailman. Long story short, as user mailman:
+specifies the LMTP port of Mailman. Long story short, as user mailman:
::
% chmod +t "$HOME"
diff --git a/src/mailman/mta/postfix.py b/src/mailman/mta/postfix.py
index d819533a3..6e71d0651 100644
--- a/src/mailman/mta/postfix.py
+++ b/src/mailman/mta/postfix.py
@@ -56,6 +56,8 @@ class LMTP:
# Locate and read the Postfix specific configuration file.
mta_config = external_configuration(config.mta.configuration)
self.postmap_command = mta_config.get('postfix', 'postmap_command')
+ self.transport_file_type = mta_config.get(
+ 'postfix', 'transport_file_type')
def create(self, mlist):
"""See `IMailTransportAgentLifecycle`."""
@@ -84,19 +86,21 @@ class LMTP:
self._generate_domains_file(fp)
# Atomically rename to the intended path.
os.rename(domains_path_new, domains_path)
- # Now, run the postmap command on both newly generated files. If
- # one files, still try the other one.
- errors = []
- for path in (lmtp_path, domains_path):
- command = self.postmap_command + ' ' + path
- status = (os.system(command) >> 8) & 0xff
- if status:
- msg = 'command failure: %s, %s, %s'
- errstr = os.strerror(status)
- log.error(msg, command, status, errstr)
- errors.append(msg % (command, status, errstr))
- if errors:
- raise RuntimeError(NL.join(errors))
+ # If the transport_file_type is 'hash' then run the postmap command
+ # on newly generated file to convert them in to hash table like
+ # Postfix wants.
+ if self.transport_file_type == 'hash':
+ errors = []
+ for path in (lmtp_path, domains_path):
+ command = self.postmap_command + ' ' + path
+ status = (os.system(command) >> 8) & 0xff
+ if status:
+ msg = 'command failure: %s, %s, %s'
+ errstr = os.strerror(status)
+ log.error(msg, command, status, errstr)
+ errors.append(msg % (command, status, errstr))
+ if errors:
+ raise RuntimeError(NL.join(errors))
def _generate_lmtp_file(self, fp):
# The format for Postfix's LMTP transport map is defined here:
@@ -125,12 +129,23 @@ class LMTP:
file=fp)
for mlist in sorted(by_domain[domain], key=sort_key):
aliases = list(utility.aliases(mlist))
- width = max(len(alias) for alias in aliases) + 3
- print(ALIASTMPL.format(aliases.pop(0), config, width), file=fp)
+ width = max(len(alias) for alias in aliases) + \
+ aliases[0].count('.') + 7
+ print(ALIASTMPL.format(self._decorate(aliases.pop(0)),
+ config, width), file=fp)
for alias in aliases:
- print(ALIASTMPL.format(alias, config, width), file=fp)
+ print(ALIASTMPL.format(self._decorate(alias),
+ config, width), file=fp)
print(file=fp)
+ def _decorate(self, name):
+ # Postfix regex tables need regex matching listname or domains. This
+ # method just decorates the name to be printed in the transport map
+ # file or relay domains file.
+ if self.transport_file_type == 'regex':
+ return '/^{}$/'.format(name).replace('.', '\.')
+ return name
+
def _generate_domains_file(self, fp):
# Uniquify the domains, then sort them alphabetically.
domains = set()
@@ -145,5 +160,5 @@ class LMTP:
# you're on your own.
""".format(now().replace(microsecond=0)), file=fp)
for domain in sorted(domains):
- print('{0} {0}'.format(domain), file=fp)
+ print('{} {}'.format(self._decorate(domain), domain), file=fp)
print(file=fp)
diff --git a/src/mailman/mta/tests/test_aliases.py b/src/mailman/mta/tests/test_aliases.py
index d5cea3ecb..b8090c86d 100644
--- a/src/mailman/mta/tests/test_aliases.py
+++ b/src/mailman/mta/tests/test_aliases.py
@@ -154,15 +154,15 @@ example.com example.com
contents = _strip_header(fp.read())
self.assertMultiLineEqual(contents, """\
# Aliases which are visible only in the @example.com domain.
-test@example.com lmtp:[127.0.0.1]:9024
-test-bounces@example.com lmtp:[127.0.0.1]:9024
-test-confirm@example.com lmtp:[127.0.0.1]:9024
-test-join@example.com lmtp:[127.0.0.1]:9024
-test-leave@example.com lmtp:[127.0.0.1]:9024
-test-owner@example.com lmtp:[127.0.0.1]:9024
-test-request@example.com lmtp:[127.0.0.1]:9024
-test-subscribe@example.com lmtp:[127.0.0.1]:9024
-test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
+test@example.com lmtp:[127.0.0.1]:9024
+test-bounces@example.com lmtp:[127.0.0.1]:9024
+test-confirm@example.com lmtp:[127.0.0.1]:9024
+test-join@example.com lmtp:[127.0.0.1]:9024
+test-leave@example.com lmtp:[127.0.0.1]:9024
+test-owner@example.com lmtp:[127.0.0.1]:9024
+test-request@example.com lmtp:[127.0.0.1]:9024
+test-subscribe@example.com lmtp:[127.0.0.1]:9024
+test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
""")
def test_two_lists(self):
@@ -185,25 +185,25 @@ example.com example.com
contents = _strip_header(fp.read())
self.assertMultiLineEqual(contents, """\
# Aliases which are visible only in the @example.com domain.
-other@example.com lmtp:[127.0.0.1]:9024
-other-bounces@example.com lmtp:[127.0.0.1]:9024
-other-confirm@example.com lmtp:[127.0.0.1]:9024
-other-join@example.com lmtp:[127.0.0.1]:9024
-other-leave@example.com lmtp:[127.0.0.1]:9024
-other-owner@example.com lmtp:[127.0.0.1]:9024
-other-request@example.com lmtp:[127.0.0.1]:9024
-other-subscribe@example.com lmtp:[127.0.0.1]:9024
-other-unsubscribe@example.com lmtp:[127.0.0.1]:9024
+other@example.com lmtp:[127.0.0.1]:9024
+other-bounces@example.com lmtp:[127.0.0.1]:9024
+other-confirm@example.com lmtp:[127.0.0.1]:9024
+other-join@example.com lmtp:[127.0.0.1]:9024
+other-leave@example.com lmtp:[127.0.0.1]:9024
+other-owner@example.com lmtp:[127.0.0.1]:9024
+other-request@example.com lmtp:[127.0.0.1]:9024
+other-subscribe@example.com lmtp:[127.0.0.1]:9024
+other-unsubscribe@example.com lmtp:[127.0.0.1]:9024
-test@example.com lmtp:[127.0.0.1]:9024
-test-bounces@example.com lmtp:[127.0.0.1]:9024
-test-confirm@example.com lmtp:[127.0.0.1]:9024
-test-join@example.com lmtp:[127.0.0.1]:9024
-test-leave@example.com lmtp:[127.0.0.1]:9024
-test-owner@example.com lmtp:[127.0.0.1]:9024
-test-request@example.com lmtp:[127.0.0.1]:9024
-test-subscribe@example.com lmtp:[127.0.0.1]:9024
-test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
+test@example.com lmtp:[127.0.0.1]:9024
+test-bounces@example.com lmtp:[127.0.0.1]:9024
+test-confirm@example.com lmtp:[127.0.0.1]:9024
+test-join@example.com lmtp:[127.0.0.1]:9024
+test-leave@example.com lmtp:[127.0.0.1]:9024
+test-owner@example.com lmtp:[127.0.0.1]:9024
+test-request@example.com lmtp:[127.0.0.1]:9024
+test-subscribe@example.com lmtp:[127.0.0.1]:9024
+test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
""")
def test_two_lists_two_domains(self):
@@ -229,24 +229,102 @@ example.net example.net
contents = _strip_header(fp.read())
self.assertMultiLineEqual(contents, """\
# Aliases which are visible only in the @example.com domain.
-test@example.com lmtp:[127.0.0.1]:9024
-test-bounces@example.com lmtp:[127.0.0.1]:9024
-test-confirm@example.com lmtp:[127.0.0.1]:9024
-test-join@example.com lmtp:[127.0.0.1]:9024
-test-leave@example.com lmtp:[127.0.0.1]:9024
-test-owner@example.com lmtp:[127.0.0.1]:9024
-test-request@example.com lmtp:[127.0.0.1]:9024
-test-subscribe@example.com lmtp:[127.0.0.1]:9024
-test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
+test@example.com lmtp:[127.0.0.1]:9024
+test-bounces@example.com lmtp:[127.0.0.1]:9024
+test-confirm@example.com lmtp:[127.0.0.1]:9024
+test-join@example.com lmtp:[127.0.0.1]:9024
+test-leave@example.com lmtp:[127.0.0.1]:9024
+test-owner@example.com lmtp:[127.0.0.1]:9024
+test-request@example.com lmtp:[127.0.0.1]:9024
+test-subscribe@example.com lmtp:[127.0.0.1]:9024
+test-unsubscribe@example.com lmtp:[127.0.0.1]:9024
# Aliases which are visible only in the @example.net domain.
-other@example.net lmtp:[127.0.0.1]:9024
-other-bounces@example.net lmtp:[127.0.0.1]:9024
-other-confirm@example.net lmtp:[127.0.0.1]:9024
-other-join@example.net lmtp:[127.0.0.1]:9024
-other-leave@example.net lmtp:[127.0.0.1]:9024
-other-owner@example.net lmtp:[127.0.0.1]:9024
-other-request@example.net lmtp:[127.0.0.1]:9024
-other-subscribe@example.net lmtp:[127.0.0.1]:9024
-other-unsubscribe@example.net lmtp:[127.0.0.1]:9024
+other@example.net lmtp:[127.0.0.1]:9024
+other-bounces@example.net lmtp:[127.0.0.1]:9024
+other-confirm@example.net lmtp:[127.0.0.1]:9024
+other-join@example.net lmtp:[127.0.0.1]:9024
+other-leave@example.net lmtp:[127.0.0.1]:9024
+other-owner@example.net lmtp:[127.0.0.1]:9024
+other-request@example.net lmtp:[127.0.0.1]:9024
+other-subscribe@example.net lmtp:[127.0.0.1]:9024
+other-unsubscribe@example.net lmtp:[127.0.0.1]:9024
+""")
+
+ def test_missing_postmap_command_raises_runtime_errorr(self):
+ # Changing the postmap command to false will always
+ # return a non-zero exit code.
+ self.postfix.postmap_command = 'false'
+ # Generating postmap hash files will raise a runtimerror.
+ with self.assertRaises(RuntimeError):
+ self.postfix.regenerate(self.tempdir)
+ # Now change the command back to true will make the
+ # command run normally.
+ self.postfix.postmap_command = 'true'
+ self.postfix.regenerate(self.tempdir)
+ # There should be two files in the tempdir.
+ self.assertEqual(sorted(os.listdir(self.tempdir)),
+ ['postfix_domains', 'postfix_lmtp'])
+
+ def test_aliases_regex(self):
+ # Test aliases generation for regex maps for postfix.
+ # Set the transport map type to regex.
+ self.postfix.transport_file_type = 'regex'
+ self.postfix.regenerate(self.tempdir)
+ # The domains file just contains the example.com domain.
+ with open(os.path.join(self.tempdir, 'postfix_domains')) as fp:
+ contents = _strip_header(fp.read())
+ self.assertMultiLineEqual(contents, """\
+/^example\.com$/ example.com
+""")
+
+ # the lmtp file contains transport mapping to the lmtp server.
+ with open(os.path.join(self.tempdir, 'postfix_lmtp')) as fp:
+ contents = _strip_header(fp.read())
+ self.assertMultiLineEqual(contents, """\
+# Aliases which are visible only in the @example.com domain.
+/^test@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-bounces@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-confirm@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-join@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-leave@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-owner@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-request@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-subscribe@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-unsubscribe@example\.com$/ lmtp:[127.0.0.1]:9024
+""")
+
+ def test_aliases_regex_with_dots(self):
+ # Test regex is generated for listnames with multiple dots.
+ self.postfix.transport_file_type = 'regex'
+ create_list('test.list.name.dots@example.com')
+ self.postfix.regenerate(self.tempdir)
+ with open(os.path.join(self.tempdir, 'postfix_domains')) as fp:
+ contents = _strip_header(fp.read())
+ self.assertMultiLineEqual(contents, """\
+/^example\.com$/ example.com
+""")
+ with open(os.path.join(self.tempdir, 'postfix_lmtp')) as fp:
+ contents = _strip_header(fp.read())
+ self.assertMultiLineEqual(contents, """\
+# Aliases which are visible only in the @example.com domain.
+/^test@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-bounces@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-confirm@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-join@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-leave@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-owner@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-request@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-subscribe@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test-unsubscribe@example\.com$/ lmtp:[127.0.0.1]:9024
+
+/^test\.list\.name\.dots@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-bounces@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-confirm@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-join@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-leave@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-owner@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-request@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-subscribe@example\.com$/ lmtp:[127.0.0.1]:9024
+/^test\.list\.name\.dots-unsubscribe@example\.com$/ lmtp:[127.0.0.1]:9024
""")
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index f08b81fca..dbed1fccf 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -106,6 +106,7 @@ class ConfigLayer(MockAndMonkeyLayer):
print(dedent("""
[postfix]
postmap_command: true
+ transport_file_type: hash
"""), file=fp)
test_config = dedent("""
[mailman]