Flexible Python-based mailing list daemon
Project description
listenwicht
listenwicht is a Python-based, highly flexible client that is able to resend email according to static redistribution rules. In particular, compared to the exceptionally simple forwarding configuration, listenwicht actually has an MTA deliver the mail locally in mboxdir format, where they are picked up and resent. Before resending, they can be rewritten. This is useful when your MTA is not a valid according to the set SPF rules. It allows resending from a SPF-legal "From" address.
Dependencies
listenwicht depends on the command-line utility inotifywatch in order to
monitor the list directory.
Caveats
listenwicht monitors in the new/ subdirectory for CREATE and MOVE_TO
permissions. This is needed because while in newer systems, MOVE_TO is called
while on older systems CREATE (without any MODIFY or WRITE_CLOSE) is
used. This means that when testing and writing a file to the new/
subdirectory in a non-atomic fashion (e.g., echo foo >mail.txt), listenwicht
will misbehave (it might probably try to parse an empty/partial file).
Usage: MTA setup
First, configure your MTA to perform delivery of emails in a maildir spool
directory. For example, in main.cf:
mail_spool_directory = /var/spool/mail/
Note the trailing slash for the spool directory, which forces maildir mode.
Then, typically, you will want to redirect traffic to a virtual user. For
example, consider there is a mailinglist user that listenwicht will run
under. Then, install the Postfix-PCRE module and create a
/etc/postfix/virtual.pcre file:
/^inf[0-9][0-9]...?[0-9]@my-mailserver\.de$/ mailinglist
/^inf-sgl@my-mailserver\.de$/ mailinglist
This will redirect all traffic to these particular addresses to the maildir
/var/spool/mail/mailinglist/new. Enable that PCRE virtual address table by
putting in your main.cf:
virtual_alias_maps = pcre:/etc/postfix/virtual.pcre
Lastly, if you want to filter by From addresses, make sure SPF is enabled in
your Postfix configuration for local deliveries.
At this stage, try if you can send mails to postfix and they get accepted. For
example, using the above configuration, write an email to
inf99abc4@my-mailserver.de and check the logs to see everything works
properly (SPF configuration, local delivery in the expected directory).
Usage: listenwicht configuration
When the mails are correctly delivered, you need to setup a listenwicht
configuration file, which is in JSON format. This is an example file, provided
in listenwicht.json:
{
"bounces": {
"via": "smtp://127.0.0.1",
"from": [ "listenwicht Bounces", "mailinglist-bounce@my-mailserver.de" ],
"admin_email": "admin@my-mailserver.de"
},
"mailing_lists": [
{
"name": "23CS1",
"conditions": [
{ "condtype": "match-header", "key": "Message-ID", "regex": "<.*@listenwicht>", "invert": true },
{ "condtype": "match-address", "key": "To", "regex": ".*bounce.*", "invert": true },
{ "condtype": "match-address", "key": "To", "regex": "inf(23cs1|23cs|23)@.*" },
{ "condtype": "match-address", "key": "From", "regex": ".*@allowed-users.de", "error": "Your 'From' address is not authorized to post on the ${mailing_list_name} mailing list." }
],
"actions": [
{ "action": "remove-header", "key": [ "Autocrypt", "DKIM-Signature", "Reply-To", "Return-Path", "X-Original-To", "Delivered-To", "Authentication-Results" ] },
{ "action": "rename-header", "old": "From", "new": "Reply-To" },
{ "action": "set-address", "key": "From", "value": [ [ "My Mailing List", "mailinglist@my-mailserver.de" ] ] },
{ "action": "set-address", "key": "To", "value": [ [ "My Mailing List", "mailinglist@my-mailserver.de" ] ] },
{ "action": "set-header", "key": "List-Id", "value": "inf23cs1@my-mailserver.de" },
{ "action": "set-address", "key": "Bcc", "value": [ [ "Cee Ess One", "cs1@foobar.com"], [ "Next cs1 usr", "other@cs1.de" ] ] },
{ "action": "deliver", "via": "smtp://127.0.0.1" }
]
},
{
"name": "23CS2",
"conditions": [
{ "condtype": "match-header", "key": "Message-ID", "regex": "<.*@listenwicht>", "invert": true },
{ "condtype": "match-address", "key": "To", "regex": ".*bounce.*", "invert": true },
{ "condtype": "match-address", "key": "To", "regex": "inf(23cs2|23cs|23)@.*" },
{ "condtype": "match-address", "key": "From", "regex": ".*@allowed-users.de", "error": "Your 'From' address is not authorized to post on the ${mailing_list_name} mailing list." }
],
"actions": [
{ "action": "remove-header", "key": [ "Autocrypt", "DKIM-Signature", "Reply-To", "Return-Path", "X-Original-To", "Delivered-To", "Authentication-Results" ] },
{ "action": "rename-header", "old": "From", "new": "Reply-To" },
{ "action": "set-address", "key": "From", "value": [ [ "My Mailing List", "mailinglist@my-mailserver.de" ] ] },
{ "action": "set-address", "key": "To", "value": [ [ "My Mailing List", "mailinglist@my-mailserver.de" ] ] },
{ "action": "set-header", "key": "List-Id", "value": "inf23cs2@my-mailserver.de" },
{ "action": "set-address", "key": "Bcc", "value": [ [ "CS2 user", "cs2-user@foo.org"], [ "Other CS2 User", "cs2-user-2@invalid.com" ] ] },
{ "action": "deliver", "via": "smtp://127.0.0.1" }
]
},
{
"name": "SGL",
"conditions": [
{ "condtype": "match-header", "key": "Message-ID", "regex": "<.*@listenwicht>", "invert": true },
{ "condtype": "match-address", "key": "To", "regex": ".*bounce.*", "invert": true },
{ "condtype": "match-address", "key": "To", "regex": "inf-sgl@.*" },
{ "condtype": "match-address", "key": "From", "regex": ".*@allowed-users.de", "error": "Your 'From' address is not authorized to post on the ${mailing_list_name} mailing list." }
],
"actions": [
{ "action": "remove-header", "key": [ "Autocrypt", "DKIM-Signature", "Reply-To", "Return-Path", "X-Original-To", "Delivered-To", "Authentication-Results" ] },
{ "action": "set-address", "key": "Reply-To", "value": [ [ "My Mailing List", "inf-sgl@my-mailserver.de" ] ] },
{ "action": "set-address", "key": "From", "value": [ [ "My Mailing List", "mailinglist@my-mailserver.de" ] ] },
{ "action": "set-address", "key": "To", "value": [ [ "My Mailing List", "mailinglist@my-mailserver.de" ] ] },
{ "action": "set-header", "key": "List-Id", "value": "inf-sgl@my-mailserver.de" },
{ "action": "set-address", "key": "Bcc", "value": [ [ "Admin One", "admin1@foo.org"], [ "Admin Two", "admin2@bar.com" ] ] },
{ "action": "deliver", "via": "smtp://127.0.0.1" }
]
}
]
}
Note that the first two mailing list configurations are very similar. The only
difference is that they match different To addresses and, therefore,
constitute different lists (with different recipients). When a new email comes
in, it is matched against all lists. If all conditions of a list apply, the
respective actions are executed.
This is the explanation of the conditions presented in the first mailing list:
- The first condition of the first list checks that the
Message-IDheader is not ending in@listenwicht>, which may prevent mail loops. - The second condition also verifies that the
Fromaddress does not include the wordbounce. This is to prevent autoresponders (which respond to theFromaddress, not theReply-To) from being forwarded to the list. - The third condition verifies that the
Fromaddress is valid (anything fromallowed-users.demay post to the list). Note that this requires your ingress email treatment to be rigid (e.g., ensure that ingress mail is not delivered on softfail or that the SPF record requires hardfail or that you have alternative authentication in place). - The last condition of the first rule verifies that the user of the
Toaddress is eitherinf23cs1,inf23cs, orinf23. Note that the last condition of the second rule only differes in that it triggers when theToaddress is eitherinf23cs2,inf23cs, orinf23. This means that sending toinf23cs1@my-mailserver.dewill trigger the first list,inf23cs2@my-mailserver.dewill trigger the second list and eitherinf23cs@my-mailserver.deorinf23@my-mailserver.dewill trigger both.
When a mailing list is triggered (because all conditions are satisfied), the actions are executed:
- Any header field in the enumeration is completely removed (e.g.,
Autocrypt,DKIM-Signature, etc). - The
Fromheader field is rewritten toReply-To. Anything that was previously aFromnow is aReply-To. - The
Fromheader field is set tomailinglist@my-mailserver.de(which is a valid address that we are allowed to send mail from) - The
Toheader field is set to that same value. - A
List-Idheader field is added - The recipients of the mail are all added as BCC.
- The mail is sent via a SMTP connection to
127.0.0.1.
Note that the example contains a third list which is slightly different in that
the Reply-To address equates to the mailing list address. This has the effect
that any reply automatically goes to all participants of the mailing list. In
contrast, a reply on the first or seconds list only goes to the original
sender.
Usage: listenwicht setup
Once you have a configuration file, you can validate the behavior by using the
listenwicht CLI display command:
$ listenwicht display example_mails/silent_discard_autoresponder
Processing mail in example_mails/silent_discard_autoresponder
Matching condition: MatchHeader field "Message-ID" does NOT match regex "<.*@listenwicht>"
Matching condition: MatchAddress field "To" does NOT match regex ".*bounce.*"
Matching condition: MatchAddress field "To" matches regex "inf(23cs1|23cs|23)@.*"
Condition result in mailing list 23CS1: condition verification failed, silently discarding
Matching condition: MatchHeader field "Message-ID" does NOT match regex "<.*@listenwicht>"
Matching condition: MatchAddress field "To" does NOT match regex ".*bounce.*"
Matching condition: MatchAddress field "To" matches regex "inf(23cs2|23cs|23)@.*"
Condition result in mailing list 23CS2: condition verification failed, silently discarding
Matching condition: MatchHeader field "Message-ID" does NOT match regex "<.*@listenwicht>"
Matching condition: MatchAddress field "To" does NOT match regex ".*bounce.*"
Matching condition: MatchAddress field "To" matches regex "inf-sgl@.*"
Condition result in mailing list SGL: condition verification failed, silently discarding
========================================================================================================================
You can see that the incoming mail triggered none of the three lists, so it
would be silently discarded. You can also see that the failing condition in all
three cases was the MatchAddress of the To header field. This means the
sender simply posted to a non-existent mailing list.
Once you have confidence that your configuration is sound, you can use the
daemon facility of listenwicht to continuously watch for incoming mail and
resend them. You can also install a systemd unit for this. First, ensure your
user has lingering enabled (as root):
# loginctl enable-linger mailinglist
Then, as user mailinglist, create a config for example at
~/.config/listenwicht.json and install the systemd unit:
$ listenwicht systemd --install --config-file ~/.config/listenwicht.json /var/spool/mail/mailinglist
That's it!
License
GNU GPL-3.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file listenwicht-0.0.2.tar.gz.
File metadata
- Download URL: listenwicht-0.0.2.tar.gz
- Upload date:
- Size: 18.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b53f7b43ba732b7c7d814daa95bf301a8f78e04a34df7ed9c515a2878d9e27e0
|
|
| MD5 |
92c0dcff66194c1c197714127021005c
|
|
| BLAKE2b-256 |
43907640247939ae54e5414e3518f9d1bf20d0fd36982b6e8d56c87709ad42f3
|