adversarial-mailman2to3/export_mailman2_settings.py

174 lines
5.9 KiB
Python

import argparse
import json
import sys
from bs4 import BeautifulSoup
from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.action import Action
from mailman.utilities.importer import NAME_MAPPINGS, member_moderation_action_mapping, dmarc_action_mapping
KEYFILTER = ('submit')
EXCLUDES = set((
'accept_these_nonmembers',
'delivery_status',
'digest_members',
'discard_these_nonmembers',
'hold_these_nonmembers',
'members',
'reject_these_nonmembers',
'user_options',
) + tuple({
# from `convert_to_uri` in mailman's `importer.py`
'goodbye_msg': 'list:user:notice:goodbye',
'msg_header': 'list:member:regular:header',
'msg_footer': 'list:member:regular:footer',
'digest_header': 'list:member:digest:header',
'digest_footer': 'list:member:digest:footer',
}.keys()) + (
# some manual settings which don't seem to be handled by the conversion
# script
'archive_volume_frequency',
'autoresponse_postings_text_upload',
'autoresponse_admin_text_upload',
'autoresponse_request_text_upload',
'bounce_notify_owner_on_bounce_increment',
'digestable',
'digest_is_default',
'mime_is_default_digest',
'digest_size_threshhold',
'_new_volume',
'_send_digest_now',
'umbrella_list',
'umbrella_member_suffix'
))
def msg(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def get_form_data(htmlfile):
data_clean = {}
soup = BeautifulSoup(htmlfile.read(), 'html.parser')
for field in soup.find_all('textarea'):
if field['name'] in EXCLUDES:
continue
name = NAME_MAPPINGS.get(field['name'], field['name'])
if 'msg' in name:
continue
data_clean[name] = [line for line in field.get_text().split('\n') if
line != ""]
for field in soup.find_all('input'):
if field['name'] in EXCLUDES:
continue
if field['type'].lower() in ('hidden', 'submit'):
continue
if field['type'].lower() == 'radio':
if 'checked' not in field.attrs:
continue
name = NAME_MAPPINGS.get(field['name'], field['name'])
if 'msg' in name:
continue
try:
value = field['value']
try:
value = int(value)
except ValueError:
...
except KeyError:
value = ''
data_clean[name] = value
return data_clean
if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog=sys.argv[0], description="Munge Mailman config data"
)
parser.add_argument("config", help="A list of files (named specifically) to import as configurations",
nargs='+',
action="append")
args = parser.parse_args()
globalconfig = {}
for page in args.config[0]:
msg(page)
with open(page) as inf:
result = get_form_data(inf)
for key, value in result.items():
if key in globalconfig:
msg(f"warning - duplicate key {key}")
else:
globalconfig[key] = value
# Handle the moderation policy.
#
# The mlist.default_member_action and mlist.default_nonmember_action enum
# values are different in Mailman 2.1, because they have been merged into a
# single enum in Mailman 3.
#
# Unmoderated lists used to have default_member_moderation set to a false
# value; this translates to the Defer default action. Moderated lists with
# the default_member_moderation set to a true value used to store the
# action in the member_moderation_action flag, the values were: 0==Hold,
# 1=Reject, 2==Discard
if bool(globalconfig.get('default_member_moderation', 0)):
globalconfig['default_member_action'] = member_moderation_action_mapping(
globalconfig.get('member_moderation_action')).name
del globalconfig['default_member_moderation']
else:
globalconfig['default_member_action'] = Action.defer.name
# Handle DMARC mitigations.
# This would be straightforward except for from_is_list. The issue
# is in MM 2.1 the from_is_list action applies if dmarc_moderation_action
# doesn't apply and they can be different.
# We will map as follows:
# from_is_list > dmarc_moderation_action
# dmarc_mitigate_action = from_is_list action
# dmarc_mitigate_unconditionally = True
# from_is_list <= dmarc_moderation_action
# dmarc_mitigate_action = dmarc_moderation_action
# dmarc_mitigate_unconditionally = False
# The text attributes are handled above.
if (globalconfig.get('from_is_list', 0) >
globalconfig.get('dmarc_moderation_action', 0)):
globalconfig['dmarc_mitigate_action'] = dmarc_action_mapping(
globalconfig.get('from_is_list', 0)).name
globalconfig['dmarc_mitigate_unconditionally'] = True
del globalconfig['from_is_list']
else:
globalconfig['dmarc_mitigate_action'] = dmarc_action_mapping(
globalconfig.get('dmarc_moderation_action', 0)).name
globalconfig['dmarc_mitigate_unconditionally'] = False
# Handle the archiving policy. In MM2.1 there were two boolean options
# but only three of the four possible states were valid. Now there's just
# an enum.
if globalconfig.get('archive'):
# For maximum safety, if for some strange reason there's no
# archive_private key, treat the list as having private archives.
if globalconfig.get('archive_private', True):
globalconfig['archive_policy'] = ArchivePolicy.private.name
del globalconfig['archive_private']
else:
globalconfig['archive_policy'] = ArchivePolicy.public.name
del globalconfig['archive']
else:
globalconfig['archive_policy'] = ArchivePolicy.never.name
print(json.dumps(globalconfig))