mailman-sync/mailman_sync.py

140 lines
3.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Update Mailman 2 lists via a json API of the form {"LIST": ["ADDRESS", ...]}
"""
import argparse
import os
from pathlib import Path
import subprocess
import tempfile
import requests
def config_list(mailman_bin: Path, mailing_list: str):
config_changes = """
# TODO: set real_name, moderator, subject prefix, and reply-to address
from_is_list = 1
anonymous_list = 1
first_strip_reply_to = 1
reply_goes_to_list = 2
# quiet member management
send_reminders = 0
send_welcome_msg = 0
send_goodbye_msg = 0
new_member_options = 272
advertised = 0
private_roster = 2 # only admins can view the roster
default_member_moderation = 1
generic_nonmember_action = 3 # discard non-member emails
forward_auto_discards = 0 # don't notify admin about discards
"""
with tempfile.NamedTemporaryFile("w", suffix=".py") as config_file:
config_file.write(config_changes)
config_file.flush()
output = subprocess.run(
[
mailman_bin / "config_list",
"--inputfile",
config_file.name,
mailing_list,
],
encoding="ascii",
capture_output=True,
check=True,
)
for line in output.stdout.splitlines():
print(f"[Configuring {mailing_list}] {line}")
def sync_members(
mailman_bin: Path, mailing_list: str, members: list[str], dry_run: bool
):
command = [
mailman_bin / "sync_members",
"--welcome-msg=no",
"--goodbye-msg=no",
"--notifyadmin=no",
"--file",
"-",
mailing_list,
]
if dry_run:
command.append("--no-change")
members_data = "\n".join(members)
output = subprocess.run(
command,
input=members_data,
encoding="ascii",
capture_output=True,
check=True,
)
for line in output.stdout.splitlines():
print(f"[Syncing {mailing_list}] {line}")
def main(mailman_bin: Path, api: str, api_auth: str, list_suffix: str, dry_run: bool):
r = requests.get(api, headers={"Authorization": api_auth})
if not r.ok:
print(f"Failed to get mailing list data from api: {r.status_code} {r.text}")
return
existing_lists = subprocess.run(
[mailman_bin / "list_lists", "-b"],
encoding="ascii",
capture_output=True,
check=True,
).stdout.split("\n")
certification_lists = r.json()
for name, members in certification_lists.items():
list_name = name + list_suffix
if list_name in existing_lists:
print(f"Configuring/syncing {list_name}...")
config_list(mailman_bin, list_name)
sync_members(mailman_bin, list_name, members, dry_run)
else:
print(f"Skipping {list_name}, as it does not exist in Mailman")
def parse_arguments():
argp = argparse.ArgumentParser(description=__doc__)
argp.add_argument(
"--bin",
default="/usr/local/mailman",
type=Path,
help="Path to Mailman site admin scripts (default %(default)s)",
)
argp.add_argument("--api", required=True, help="API endpoint to retrieve JSON from")
argp.add_argument("--list-suffix", help="Suffix for mailing lists")
argp.add_argument(
"-n",
"--dry-run",
action="store_true",
help="Don't make changes, just print what would happen",
)
return argp.parse_args()
if __name__ == "__main__":
args = parse_arguments()
if "API_AUTH" in os.environ:
api_auth = os.environ.get("API_AUTH")
else:
print("Missing API_AUTH environment variable")
exit(-1)
try:
main(args.bin, args.api, api_auth, args.list_suffix, args.dry_run)
except subprocess.CalledProcessError as e:
print(e.stderr)
raise