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', 'nondigestable', 'digestable', 'digest_is_default', 'mime_is_default_digest', 'digest_size_threshhold', '_new_volume', '_send_digest_now', 'umbrella_list', 'umbrella_member_suffix', 'send_reminders', 'admin_member_chunksize', 'host_name', 'new_member_options', 'include_sender_header', 'drop_cc', 'available_languages', 'encode_ascii_prefixes', 'regular_exclude_lists', 'regular_include_lists' )) 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 else: globalconfig['dmarc_mitigate_action'] = dmarc_action_mapping( globalconfig.get('dmarc_moderation_action', 0)).name globalconfig['dmarc_mitigate_unconditionally'] = False if 'from_is_list' in globalconfig.keys(): del globalconfig['from_is_list'] # 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))