2022-12-24 14:04:29 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
"""
|
|
|
|
Update Mailman 2 lists via a json API of the form {"LIST": ["ADDRESS", ...]}
|
|
|
|
"""
|
|
|
|
|
|
|
|
import argparse
|
2023-01-05 21:47:43 -05:00
|
|
|
import os
|
2022-12-24 14:04:29 -05:00
|
|
|
from pathlib import Path
|
2023-01-09 21:37:27 -05:00
|
|
|
import secrets
|
|
|
|
import string
|
2022-12-24 14:04:29 -05:00
|
|
|
import subprocess
|
2023-01-09 16:25:28 -05:00
|
|
|
import tempfile
|
2022-12-24 14:04:29 -05:00
|
|
|
|
|
|
|
import requests
|
|
|
|
|
2023-01-09 21:37:27 -05:00
|
|
|
PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation
|
|
|
|
PASSWORD_LEN = 18
|
|
|
|
|
|
|
|
|
|
|
|
def newlist(mailman_bin: Path, listname: str, urlhost: str, admin: str):
|
|
|
|
password = "".join(secrets.choice(PASSWORD_CHARS) for i in range(PASSWORD_LEN))
|
|
|
|
|
|
|
|
output = subprocess.run(
|
|
|
|
[
|
|
|
|
mailman_bin / "newlist",
|
|
|
|
"--quiet",
|
|
|
|
f"--urlhost={urlhost}",
|
|
|
|
listname,
|
|
|
|
admin,
|
|
|
|
password,
|
|
|
|
],
|
|
|
|
encoding="ascii",
|
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
)
|
|
|
|
for line in output.stdout.splitlines():
|
|
|
|
print(f"[Creating {listname}] {line}")
|
|
|
|
|
2022-12-24 14:04:29 -05:00
|
|
|
|
2023-01-09 21:34:28 -05:00
|
|
|
def config_list(mailman_bin: Path, mailing_list: str, dry_run: bool):
|
2023-01-09 16:25:28 -05:00
|
|
|
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()
|
2023-01-09 21:34:28 -05:00
|
|
|
|
|
|
|
command = [
|
|
|
|
mailman_bin / "config_list",
|
|
|
|
"--inputfile",
|
|
|
|
config_file.name,
|
|
|
|
mailing_list,
|
|
|
|
]
|
|
|
|
if dry_run:
|
|
|
|
command.append("--checkonly")
|
|
|
|
|
2023-01-09 16:25:28 -05:00
|
|
|
output = subprocess.run(
|
2023-01-09 21:34:28 -05:00
|
|
|
command,
|
2023-01-09 16:25:28 -05:00
|
|
|
encoding="ascii",
|
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
)
|
|
|
|
for line in output.stdout.splitlines():
|
|
|
|
print(f"[Configuring {mailing_list}] {line}")
|
|
|
|
|
|
|
|
|
2022-12-25 02:25:24 -05:00
|
|
|
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")
|
|
|
|
|
2022-12-25 02:19:14 -05:00
|
|
|
members_data = "\n".join(members)
|
2022-12-24 14:04:29 -05:00
|
|
|
output = subprocess.run(
|
2022-12-25 02:25:24 -05:00
|
|
|
command,
|
2022-12-25 02:14:36 -05:00
|
|
|
input=members_data,
|
2022-12-25 02:19:14 -05:00
|
|
|
encoding="ascii",
|
2022-12-24 14:04:29 -05:00
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
)
|
2023-01-05 21:55:27 -05:00
|
|
|
for line in output.stdout.splitlines():
|
|
|
|
print(f"[Syncing {mailing_list}] {line}")
|
2022-12-24 14:04:29 -05:00
|
|
|
|
|
|
|
|
2023-01-09 21:37:27 -05:00
|
|
|
def main(
|
|
|
|
mailman_bin: Path,
|
|
|
|
api: str,
|
|
|
|
api_auth: str,
|
|
|
|
list_suffix: str,
|
|
|
|
dry_run: bool,
|
|
|
|
urlhost: str,
|
|
|
|
admin: str,
|
|
|
|
):
|
2023-01-05 21:47:43 -05:00
|
|
|
r = requests.get(api, headers={"Authorization": api_auth})
|
2022-12-24 14:04:29 -05:00
|
|
|
if not r.ok:
|
|
|
|
print(f"Failed to get mailing list data from api: {r.status_code} {r.text}")
|
|
|
|
return
|
|
|
|
|
|
|
|
existing_lists = subprocess.run(
|
2022-12-25 02:19:14 -05:00
|
|
|
[mailman_bin / "list_lists", "-b"],
|
|
|
|
encoding="ascii",
|
|
|
|
capture_output=True,
|
|
|
|
check=True,
|
|
|
|
).stdout.split("\n")
|
2022-12-24 14:04:29 -05:00
|
|
|
certification_lists = r.json()
|
|
|
|
for name, members in certification_lists.items():
|
2022-12-25 02:12:08 -05:00
|
|
|
list_name = name + list_suffix
|
2023-01-09 21:37:27 -05:00
|
|
|
if list_name not in existing_lists:
|
|
|
|
if dry_run:
|
|
|
|
print(f"Skipping non-existing list {list_name} in dry run mode")
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
newlist(mailman_bin, list_name, urlhost, admin)
|
2022-12-24 14:04:29 -05:00
|
|
|
|
2023-01-09 21:37:27 -05:00
|
|
|
print(f"Configuring/syncing {list_name}...")
|
|
|
|
config_list(mailman_bin, list_name, dry_run)
|
|
|
|
sync_members(mailman_bin, list_name, members, dry_run)
|
2022-12-24 14:04:29 -05:00
|
|
|
|
2023-01-09 21:26:28 -05:00
|
|
|
|
|
|
|
def parse_arguments():
|
2022-12-24 14:04:29 -05:00
|
|
|
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")
|
2023-01-09 21:37:27 -05:00
|
|
|
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"
|
|
|
|
)
|
2022-12-25 02:25:24 -05:00
|
|
|
argp.add_argument(
|
|
|
|
"-n",
|
|
|
|
"--dry-run",
|
|
|
|
action="store_true",
|
|
|
|
help="Don't make changes, just print what would happen",
|
|
|
|
)
|
2023-01-09 21:26:28 -05:00
|
|
|
return argp.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
args = parse_arguments()
|
2022-12-24 14:04:29 -05:00
|
|
|
|
2023-01-05 21:47:43 -05:00
|
|
|
if "API_AUTH" in os.environ:
|
|
|
|
api_auth = os.environ.get("API_AUTH")
|
|
|
|
else:
|
|
|
|
print("Missing API_AUTH environment variable")
|
|
|
|
exit(-1)
|
|
|
|
|
2022-12-25 02:16:54 -05:00
|
|
|
try:
|
2023-01-09 21:37:27 -05:00
|
|
|
main(
|
|
|
|
args.bin,
|
|
|
|
args.api,
|
|
|
|
api_auth,
|
|
|
|
args.list_suffix,
|
|
|
|
args.dry_run,
|
|
|
|
args.urlhost,
|
|
|
|
args.admin,
|
|
|
|
)
|
2022-12-25 02:16:54 -05:00
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
print(e.stderr)
|
|
|
|
raise
|