LTMP server

Mailman can accept messages via LMTP (RFC 2033). Most modern mail servers support LMTP local delivery, so this is a very portable way to connect Mailman with your mail server.

Our LMTP server is fairly simple though; all it does is make sure that the message is destined for a valid endpoint, e.g. mylist-join@example.com, that the message bytes can be parsed into a message object, and that the message has a Message-ID header.

Let’s start a testable LMTP runner.

>>> from mailman.testing import helpers
>>> master = helpers.TestableMaster()
>>> master.start('lmtp')

It also helps to have a nice LMTP client.

>>> lmtp = helpers.get_lmtp_client()
(220, b'... GNU Mailman LMTP runner 1.1')
>>> lmtp.lhlo('remote.example.org')
(250, ...)

Posting address

Once the mailing list is created, the posting address is valid, and messages can be sent to the list.

>>> create_list('mylist@example.com')
<mailing list "mylist@example.com" at ...>

>>> transaction.commit()
>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist@example.com'], """\
... From: anne.person@example.com
... To: mylist@example.com
... Subject: An interesting message
... Message-ID: <badger>
...
... This is an interesting message.
... """)
{}

Since the message itself is valid, it gets parsed and lands in the incoming queue.

>>> from mailman.testing.helpers import get_queue_messages
>>> messages = get_queue_messages('in')
>>> len(messages)
1
>>> print(messages[0].msg.as_string())
From: anne.person@example.com
To: mylist@example.com
Subject: An interesting message
Message-ID: <badger>
X-Message-ID-Hash: JYMZWSQ4IC2JPKK7ZUONRFRVC4ZYJGKJ
X-MailFrom: anne.person@example.com

This is an interesting message.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
to_list      : True
version      : ...

Sub-addresses

The LMTP server understands each of the list’s sub-addreses, such as -join, -leave, -request and so on. The message is accepted if posted to a valid sub-address.

>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist-request@example.com'], """\
... From: anne.person@example.com
... To: mylist-request@example.com
... Subject: Help
... Message-ID: <dog>
...
... Please help me.
... """)
{}

Request subaddress

Depending on the subaddress, there is a message in the appropriate queue for later processing. For example, all -request messages are put into the command queue for processing.

>>> messages = get_queue_messages('command')
>>> len(messages)
1
>>> print(messages[0].msg.as_string())
From: anne.person@example.com
To: mylist-request@example.com
Subject: Help
Message-ID: <dog>
X-Message-ID-Hash: 4SKREUSPI62BHDMFBSOZ3BMXFETSQHNA
X-MailFrom: anne.person@example.com

Please help me.
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
subaddress   : request
version      : ...

Bounce processor

A message to the -bounces address goes to the bounce processor.

>>> lmtp.sendmail(
...     'mail-daemon@example.com',
...     ['mylist-bounces@example.com'], """\
... From: mail-daemon@example.com
... To: mylist-bounces@example.com
... Subject: A bounce
... Message-ID: <elephant>
...
... Bouncy bouncy.
... """)
{}
>>> messages = get_queue_messages('bounces')
>>> len(messages)
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
subaddress   : bounces
version      : ...

Command processor

Confirmation messages go to the command processor...

>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist-confirm@example.com'], """\
... From: anne.person@example.com
... To: mylist-confirm@example.com
... Subject: A bounce
... Message-ID: <falcon>
...
... confirm 123
... """)
{}
>>> messages = get_queue_messages('command')
>>> len(messages)
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
subaddress   : confirm
version      : ...

...as do join messages...

>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist-join@example.com'], """\
... From: anne.person@example.com
... To: mylist-join@example.com
... Message-ID: <giraffe>
...
... """)
{}
>>> messages = get_queue_messages('command')
>>> len(messages)
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
subaddress   : join
version      : ...

>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist-subscribe@example.com'], """\
... From: anne.person@example.com
... To: mylist-subscribe@example.com
... Message-ID: <hippopotamus>
...
... """)
{}
>>> messages = get_queue_messages('command')
>>> len(messages)
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
subaddress   : join
version      : ...

...and leave messages.

>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist-leave@example.com'], """\
... From: anne.person@example.com
... To: mylist-leave@example.com
... Message-ID: <iguana>
...
... """)
{}
>>> messages = get_queue_messages('command')
>>> len(messages)
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
subaddress   : leave
version      : ...

>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist-unsubscribe@example.com'], """\
... From: anne.person@example.com
... To: mylist-unsubscribe@example.com
... Message-ID: <jackal>
...
... """)
{}
>>> messages = get_queue_messages('command')
>>> len(messages)
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
listid       : mylist.example.com
original_size: ...
subaddress   : leave
version      : ...

Incoming processor

Messages to the -owner address also go to the incoming processor.

>>> lmtp.sendmail(
...     'anne.person@example.com',
...     ['mylist-owner@example.com'], """\
... From: anne.person@example.com
... To: mylist-owner@example.com
... Message-ID: <kangaroo>
...
... """)
{}
>>> messages = get_queue_messages('in')
>>> len(messages)
1
>>> dump_msgdata(messages[0].msgdata)
_parsemsg    : False
envsender    : noreply@example.com
listid       : mylist.example.com
original_size: ...
subaddress   : owner
to_owner     : True
version      : ...