#!/usr/bin/env python3 """ Update Mailman 2 lists via a json API of the form {"LIST": ["ADDRESS", ...]} """ import argparse from dataclasses import dataclass import os from pathlib import Path import secrets import string import subprocess import tempfile import requests PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation PASSWORD_LEN = 18 @dataclass class ListManager: mailman_bin: Path list_name: str dry_run: bool def newlist(self, urlhost: str, admin: str): password = "".join(secrets.choice(PASSWORD_CHARS) for i in range(PASSWORD_LEN)) output = subprocess.run( [ self.mailman_bin / "newlist", "--quiet", f"--urlhost={urlhost}", self.list_name, admin, password, ], encoding="ascii", capture_output=True, check=True, ) for line in output.stdout.splitlines(): print(f"[Creating {self.list_name}] {line}") def config_list(self): 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() command = [ self.mailman_bin / "config_list", "--inputfile", config_file.name, self.list_name, ] if self.dry_run: command.append("--checkonly") output = subprocess.run( command, encoding="ascii", capture_output=True, check=True, ) for line in output.stdout.splitlines(): print(f"[Configuring {self.list_name}] {line}") def sync_members(self, members: list[str]): command = [ self.mailman_bin / "sync_members", "--welcome-msg=no", "--goodbye-msg=no", "--notifyadmin=no", "--file", "-", self.list_name, ] if self.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 {self.list_name}] {line}") def main( mailman_bin: Path, api: str, api_auth: str, list_suffix: str, dry_run: bool, urlhost: str, admin: str, ): 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 list_manager = ListManager(mailman_bin, list_name, dry_run) if list_name not in existing_lists: if dry_run: print(f"Skipping non-existing list {list_name} in dry run mode") continue else: list_manager.newlist(urlhost, admin) print(f"Configuring/syncing {list_name}...") list_manager.config_list() list_manager.sync_members(members) 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("--urlhost", help="Urlhost to use when creating new lists") argp.add_argument( "--admin", help="Admin email address to use when creating new 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, args.urlhost, args.admin, ) except subprocess.CalledProcessError as e: print(e.stderr) raise