Ejemplo n.º 1
0
    def __init__(self, name, queue_directory,
                 slice=None, numslices=1, recover=False):
        """Create a switchboard object.

        :param name: The queue name.
        :type name: str
        :param queue_directory: The queue directory.
        :type queue_directory: str
        :param slice: The slice number for this switchboard, or None.  If not
            None, it must be [0..`numslices`).
        :type slice: int or None
        :param numslices: The total number of slices to split this queue
            directory into.  It must be a power of 2.
        :type numslices: int
        :param recover: True if backup files should be recovered.
        :type recover: bool
        """
        assert (numslices & (numslices - 1)) == 0, (
            'Not a power of 2: {0}'.format(numslices))
        self.name = name
        self.queue_directory = queue_directory
        # If configured to, create the directory if it doesn't yet exist.
        if config.create_paths:
            makedirs(self.queue_directory, 0770)
        # Fast track for no slices
        self._lower = None
        self._upper = None
        # BAW: test performance and end-cases of this algorithm
        if numslices <> 1:
            self._lower = ((shamax + 1) * slice) / numslices
            self._upper = (((shamax + 1) * (slice + 1)) / numslices) - 1
        if recover:
            self.recover_backup_files()
Ejemplo n.º 2
0
    def test_makedirs_race_condition(self):
        """Mocks os.makedirs behaviour to create willingly a race condition in
        filesystem.makedirs and test it."""

        first_inexistent_directory(self.baz)

        makedirs(self.bar)

        with unittest.mock.patch('os.makedirs', new=fake_makedirs):
            with self.assertRaises(FileExistsError):
                makedirs(self.baz)
Ejemplo n.º 3
0
 def test_unicode_in_default(self):
     # What if the default template is already in UTF-8?   For example, if
     # you import it twice.
     footer = b'\xe4\xb8\xad $listinfo_uri'
     footer_path = os.path.join(config.VAR_DIR, 'templates', 'lists',
                                '*****@*****.**', 'en', 'footer.txt')
     makedirs(os.path.dirname(footer_path))
     with open(footer_path, 'wb') as fp:
         fp.write(footer)
     self._pckdict['msg_footer'] = b'NEW-VALUE'
     import_config_pck(self._mlist, self._pckdict)
     text = decorate('list:member:regular:footer', self._mlist)
     self.assertEqual(text, 'NEW-VALUE')
Ejemplo n.º 4
0
 def add(self, store, message):
     # Ensure that the message has the requisite headers.
     message_ids = message.get_all('message-id', [])
     if len(message_ids) != 1:
         raise ValueError('Exactly one Message-ID header required')
     # Calculate and insert the X-Message-ID-Hash.
     message_id = message_ids[0]
     if isinstance(message_id, bytes):
         message_id = message_id.decode('ascii')
     # Complain if the Message-ID already exists in the storage.
     existing = store.query(Message).filter(
         Message.message_id == message_id).first()
     if existing is not None:
         raise ValueError(
             'Message ID already exists in message store: {0}'.format(
                 message_id))
     shaobj = hashlib.sha1(message_id.encode('utf-8'))
     hash32 = base64.b32encode(shaobj.digest()).decode('utf-8')
     del message['X-Message-ID-Hash']
     message['X-Message-ID-Hash'] = hash32
     # Calculate the path on disk where we're going to store this message
     # object, in pickled format.
     parts = []
     split = list(hash32)
     while split and len(parts) < MAX_SPLITS:
         parts.append(split.pop(0) + split.pop(0))
     parts.append(hash32)
     relpath = os.path.join(*parts)
     # Store the message in the database.  This relies on the database
     # providing a unique serial number, but to get this information, we
     # have to use a straight insert instead of relying on Elixir to create
     # the object.
     Message(message_id=message_id,
             message_id_hash=hash32,
             path=relpath)
     # Now calculate the full file system path.
     path = os.path.join(config.MESSAGES_DIR, relpath)
     # Write the file to the path, but catch the appropriate exception in
     # case the parent directories don't yet exist.  In that case, create
     # them and try again.
     while True:
         try:
             with open(path, 'wb') as fp:
                 # -1 says to use the highest protocol available.
                 pickle.dump(message, fp, -1)
                 break
         except IOError as error:
             if error.errno != errno.ENOENT:
                 raise
         makedirs(os.path.dirname(path))
     return hash32
Ejemplo n.º 5
0
 def test_unicode_in_default(self):
     # What if the default template is already in UTF-8?   For example, if
     # you import it twice.
     footer = b'\xe4\xb8\xad $listinfo_uri'
     footer_path = os.path.join(
         config.VAR_DIR, 'templates', 'lists',
         '*****@*****.**', 'en', 'footer-generic.txt')
     makedirs(os.path.dirname(footer_path))
     with open(footer_path, 'wb') as fp:
         fp.write(footer)
     self._pckdict[b'msg_footer'] = b'NEW-VALUE'
     import_config_pck(self._mlist, self._pckdict)
     text = decorate(self._mlist, self._mlist.footer_uri)
     self.assertEqual(text, 'NEW-VALUE')
Ejemplo n.º 6
0
 def add(self, store, message):
     # Ensure that the message has the requisite headers.
     message_ids = message.get_all('message-id', [])
     if len(message_ids) != 1:
         raise ValueError('Exactly one Message-ID header required')
     # Calculate and insert the X-Message-ID-Hash.
     message_id = message_ids[0]
     if isinstance(message_id, bytes):
         message_id = message_id.decode('ascii')
     # Complain if the Message-ID already exists in the storage.
     existing = store.query(Message).filter(
         Message.message_id == message_id).first()
     if existing is not None:
         raise ValueError(
             'Message ID already exists in message store: {0}'.format(
                 message_id))
     shaobj = hashlib.sha1(message_id.encode('utf-8'))
     hash32 = base64.b32encode(shaobj.digest()).decode('utf-8')
     del message['X-Message-ID-Hash']
     message['X-Message-ID-Hash'] = hash32
     # Calculate the path on disk where we're going to store this message
     # object, in pickled format.
     parts = []
     split = list(hash32)
     while split and len(parts) < MAX_SPLITS:
         parts.append(split.pop(0) + split.pop(0))
     parts.append(hash32)
     relpath = os.path.join(*parts)
     # Store the message in the database.  This relies on the database
     # providing a unique serial number, but to get this information, we
     # have to use a straight insert instead of relying on Elixir to create
     # the object.
     Message(message_id=message_id, message_id_hash=hash32, path=relpath)
     # Now calculate the full file system path.
     path = os.path.join(config.MESSAGES_DIR, relpath)
     # Write the file to the path, but catch the appropriate exception in
     # case the parent directories don't yet exist.  In that case, create
     # them and try again.
     while True:
         try:
             with open(path, 'wb') as fp:
                 # -1 says to use the highest protocol available.
                 pickle.dump(message, fp, -1)
                 break
         except IOError as error:
             if error.errno != errno.ENOENT:
                 raise
         makedirs(os.path.dirname(path))
     return hash32
Ejemplo n.º 7
0
 def __init__(self, fqdn_listname):
     super(MailingList, self).__init__()
     listname, at, hostname = fqdn_listname.partition('@')
     assert hostname, 'Bad list name: {0}'.format(fqdn_listname)
     self.list_name = listname
     self.mail_host = hostname
     self._list_id = '{0}.{1}'.format(listname, hostname)
     # For the pending database
     self.next_request_id = 1
     # We need to set up the rosters.  Normally, this method will get called
     # when the MailingList object is loaded from the database, but when the
     # constructor is called, SQLAlchemy's `load` event isn't triggered.
     # Thus we need to set up the rosters explicitly.
     self._post_load()
     makedirs(self.data_path)
 def __init__(self, fqdn_listname):
     super().__init__()
     listname, at, hostname = fqdn_listname.partition('@')
     assert hostname, 'Bad list name: {0}'.format(fqdn_listname)
     self.list_name = listname
     self.mail_host = hostname
     self._list_id = '{0}.{1}'.format(listname, hostname)
     # For the pending database
     self.next_request_id = 1
     # We need to set up the rosters.  Normally, this method will get called
     # when the MailingList object is loaded from the database, but when the
     # constructor is called, SQLAlchemy's `load` event isn't triggered.
     # Thus we need to set up the rosters explicitly.
     self._post_load()
     makedirs(self.data_path)
Ejemplo n.º 9
0
 def __init__(self, fqdn_listname):
     super(MailingList, self).__init__()
     listname, at, hostname = fqdn_listname.partition('@')
     assert hostname, 'Bad list name: {0}'.format(fqdn_listname)
     self.list_name = listname
     self.mail_host = hostname
     self._list_id = '{0}.{1}'.format(listname, hostname)
     # For the pending database
     self.next_request_id = 1
     # We need to set up the rosters.  Normally, this method will get
     # called when the MailingList object is loaded from the database, but
     # that's not the case when the constructor is called.  So, set up the
     # rosters explicitly.
     self.__storm_loaded__()
     makedirs(self.data_path)
Ejemplo n.º 10
0
 def __init__(self, fqdn_listname):
     super(MailingList, self).__init__()
     listname, at, hostname = fqdn_listname.partition('@')
     assert hostname, 'Bad list name: {0}'.format(fqdn_listname)
     self.list_name = listname
     self.mail_host = hostname
     # For the pending database
     self.next_request_id = 1
     # We need to set up the rosters.  Normally, this method will get
     # called when the MailingList object is loaded from the database, but
     # that's not the case when the constructor is called.  So, set up the
     # rosters explicitly.
     self.__storm_loaded__()
     self.personalize = Personalization.none
     self.display_name = string.capwords(
         SPACE.join(listname.split(UNDERSCORE)))
     makedirs(self.data_path)
Ejemplo n.º 11
0
 def ensure_directories_exist(self):
     """Create all path directories if they do not exist."""
     if self.create_paths:
         for variable, directory in self.paths.items():
             makedirs(directory)
         # Avoid circular imports.
         from mailman.utilities.datetime import now
         # Create a mailman.cfg template file if it doesn't already exist.
         # LBYL: <boo hiss>, but it's probably okay because the directories
         # likely didn't exist before the above loop, and we'll create a
         # temporary lock.
         lock_file = os.path.join(self.LOCK_DIR, 'mailman-cfg.lck')
         mailman_cfg = os.path.join(self.ETC_DIR, 'mailman.cfg')
         with Lock(lock_file):
             if not os.path.exists(mailman_cfg):
                 with open(mailman_cfg, 'w') as fp:
                     print(MAILMAN_CFG_TEMPLATE.format(
                         now().replace(microsecond=0)), file=fp)
Ejemplo n.º 12
0
 def ensure_directories_exist(self):
     """Create all path directories if they do not exist."""
     if self.create_paths:
         for variable, directory in self.paths.items():
             makedirs(directory)
         # Avoid circular imports.
         from mailman.utilities.datetime import now
         # Create a mailman.cfg template file if it doesn't already exist.
         # LBYL: <boo hiss>, but it's probably okay because the directories
         # likely didn't exist before the above loop, and we'll create a
         # temporary lock.
         lock_file = os.path.join(self.LOCK_DIR, 'mailman-cfg.lck')
         mailman_cfg = os.path.join(self.ETC_DIR, 'mailman.cfg')
         with Lock(lock_file):
             if not os.path.exists(mailman_cfg):
                 with open(mailman_cfg, 'w') as fp:
                     print(MAILMAN_CFG_TEMPLATE.format(
                         now().replace(microsecond=0)), file=fp)
Ejemplo n.º 13
0
    def __init__(self,
                 name,
                 queue_directory,
                 slice=None,
                 numslices=1,
                 recover=False):
        """Create a switchboard object.

        :param name: The queue name.
        :type name: str
        :param queue_directory: The queue directory.
        :type queue_directory: str
        :param slice: The slice number for this switchboard, or None.  If not
            None, it must be [0..`numslices`).
        :type slice: int or None
        :param numslices: The total number of slices to split this queue
            directory into.  It must be a power of 2.
        :type numslices: int
        :param recover: True if backup files should be recovered.
        :type recover: bool
        """
        assert (numslices & (numslices - 1)) == 0, (
            'Not a power of 2: {}'.format(numslices))
        self.name = name
        self.queue_directory = queue_directory
        # If configured to, create the directory if it doesn't yet exist.
        if config.create_paths:
            makedirs(self.queue_directory, 0o770)
        # Fast track for no slices
        self._lower = None
        self._upper = None
        # BAW: test performance and end-cases of this algorithm
        if numslices != 1:
            self._lower = ((shamax + 1) * slice) / numslices
            self._upper = (((shamax + 1) * (slice + 1)) / numslices) - 1
        if recover:
            self.recover_backup_files()
Ejemplo n.º 14
0
def import_config_pck(mlist, config_dict):
    """Apply a config.pck configuration dictionary to a mailing list.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param config_dict: The Mailman 2.1 configuration dictionary.
    :type config_dict: dict
    """
    for key, value in config_dict.items():
        # Some attributes must not be directly imported.
        if key in EXCLUDES:
            continue
        # These objects need explicit type conversions.
        if key in DATETIME_COLUMNS:
            continue
        # Some attributes from Mailman 2 were renamed in Mailman 3.
        key = NAME_MAPPINGS.get(key, key)
        # Handle the simple case where the key is an attribute of the
        # IMailingList and the types are the same (modulo 8-bit/unicode
        # strings).
        #
        # If the mailing list has a preferred language that isn't registered
        # in the configuration file, hasattr() will swallow the KeyError this
        # raises and return False.  Treat that attribute specially.
        if key == 'preferred_language' or hasattr(mlist, key):
            if isinstance(value, bytes):
                value = bytes_to_str(value)
            # Some types require conversion.
            converter = TYPES.get(key)
            if converter is None:
                column = getattr(mlist.__class__, key, None)
                if column is not None and isinstance(column.type, Boolean):
                    converter = bool
            try:
                if converter is not None:
                    value = converter(value)
                setattr(mlist, key, value)
            except (TypeError, KeyError):
                print('Type conversion error for key "{}": {}'.format(
                    key, value),
                      file=sys.stderr)
    for key in DATETIME_COLUMNS:
        try:
            value = datetime.datetime.utcfromtimestamp(config_dict[key])
        except KeyError:
            continue
        if key == 'last_post_time':
            setattr(mlist, 'last_post_at', value)
            continue
        setattr(mlist, 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(config_dict.get('default_member_moderation', 0)):
        mlist.default_member_action = member_moderation_action_mapping(
            config_dict.get('member_moderation_action'))
    else:
        mlist.default_member_action = Action.defer
    # 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 config_dict.get('archive'):
        # For maximum safety, if for some strange reason there's no
        # archive_private key, treat the list as having private archives.
        if config_dict.get('archive_private', True):
            mlist.archive_policy = ArchivePolicy.private
        else:
            mlist.archive_policy = ArchivePolicy.public
    else:
        mlist.archive_policy = ArchivePolicy.never
    # Handle ban list.
    ban_manager = IBanManager(mlist)
    for address in config_dict.get('ban_list', []):
        ban_manager.ban(bytes_to_str(address))
    # Handle acceptable aliases.
    acceptable_aliases = config_dict.get('acceptable_aliases', '')
    if isinstance(acceptable_aliases, bytes):
        acceptable_aliases = acceptable_aliases.decode('utf-8')
    if isinstance(acceptable_aliases, str):
        acceptable_aliases = acceptable_aliases.splitlines()
    alias_set = IAcceptableAliasSet(mlist)
    for address in acceptable_aliases:
        address = address.strip()
        if len(address) == 0:
            continue
        address = bytes_to_str(address)
        try:
            alias_set.add(address)
        except ValueError:
            # When .add() rejects this, the line probably contains a regular
            # expression.  Make that explicit for MM3.
            alias_set.add('^' + address)
    # Handle conversion to URIs.  In MM2.1, the decorations are strings
    # containing placeholders, and there's no provision for language-specific
    # templates.  In MM3, template locations are specified by URLs with the
    # special `mailman:` scheme indicating a file system path.  What we do
    # here is look to see if the list's decoration is different than the
    # default, and if so, we'll write the new decoration template to a
    # `mailman:` scheme path.
    convert_to_uri = {
        'welcome_msg': 'welcome_message_uri',
        'goodbye_msg': 'goodbye_message_uri',
        'msg_header': 'header_uri',
        'msg_footer': 'footer_uri',
        'digest_header': 'digest_header_uri',
        'digest_footer': 'digest_footer_uri',
    }
    # The best we can do is convert only the most common ones.  These are
    # order dependent; the longer substitution with the common prefix must
    # show up earlier.
    convert_placeholders = [
        ('%(real_name)s@%(host_name)s', '$fqdn_listname'),
        ('%(real_name)s', '$display_name'),
        ('%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s',
         '$listinfo_uri'),
    ]
    # Collect defaults.
    defaults = {}
    for oldvar, newvar in convert_to_uri.items():
        default_value = getattr(mlist, newvar, None)
        if not default_value:
            continue
        # Check if the value changed from the default.
        try:
            default_text = decorate(mlist, default_value)
        except (URLError, KeyError):
            # Use case: importing the old [email protected] into [email protected].  We can't
            # check if it changed from the default so don't import, we may do
            # more harm than good and it's easy to change if needed.
            # TESTME
            print('Unable to convert mailing list attribute:',
                  oldvar,
                  'with old value "{}"'.format(default_value),
                  file=sys.stderr)
            continue
        defaults[newvar] = (default_value, default_text)
    for oldvar, newvar in convert_to_uri.items():
        if oldvar not in config_dict:
            continue
        text = config_dict[oldvar]
        if isinstance(text, bytes):
            text = text.decode('utf-8', 'replace')
        for oldph, newph in convert_placeholders:
            text = text.replace(oldph, newph)
        default_value, default_text = defaults.get(newvar, (None, None))
        if not text and not (default_value or default_text):
            # Both are empty, leave it.
            continue
        # Check if the value changed from the default
        try:
            expanded_text = decorate_template(mlist, text)
        except KeyError:
            # Use case: importing the old [email protected] into [email protected]
            # We can't check if it changed from the default
            # -> don't import, we may do more harm than good and it's easy to
            # change if needed
            # TESTME
            print('Unable to convert mailing list attribute:',
                  oldvar,
                  'with value "{}"'.format(text),
                  file=sys.stderr)
            continue
        if (expanded_text and default_text
                and expanded_text.strip() == default_text.strip()):
            # Keep the default.
            continue
        # Write the custom value to the right file.
        base_uri = 'mailman:///$listname/$language/'
        if default_value:
            filename = default_value.rpartition('/')[2]
        else:
            filename = '{}.txt'.format(newvar[:-4])
        if not default_value or not default_value.startswith(base_uri):
            setattr(mlist, newvar, base_uri + filename)
        filepath = list(search(filename, mlist))[0]
        makedirs(os.path.dirname(filepath))
        with codecs.open(filepath, 'w', encoding='utf-8') as fp:
            fp.write(text)
    # Import rosters.
    regulars_set = set(config_dict.get('members', {}))
    digesters_set = set(config_dict.get('digest_members', {}))
    members = regulars_set.union(digesters_set)
    # Don't send welcome messages when we import the rosters.
    send_welcome_message = mlist.send_welcome_message
    mlist.send_welcome_message = False
    try:
        import_roster(mlist, config_dict, members, MemberRole.member)
        import_roster(mlist, config_dict, config_dict.get('owner', []),
                      MemberRole.owner)
        import_roster(mlist, config_dict, config_dict.get('moderator', []),
                      MemberRole.moderator)
        # Now import the '*_these_nonmembers' properties, filtering out the
        # regexps which will remain in the property.
        for action_name in ('accept', 'hold', 'reject', 'discard'):
            prop_name = '{}_these_nonmembers'.format(action_name)
            emails = [
                addr for addr in config_dict.get(prop_name, [])
                if not addr.startswith('^')
            ]
            import_roster(mlist, config_dict, emails, MemberRole.nonmember,
                          Action[action_name])
            # Only keep the regexes in the legacy list property.
            list_prop = getattr(mlist, prop_name)
            for email in emails:
                list_prop.remove(email)
    finally:
        mlist.send_welcome_message = send_welcome_message
Ejemplo n.º 15
0
def import_config_pck(mlist, config_dict):
    """Apply a config.pck configuration dictionary to a mailing list.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param config_dict: The Mailman 2.1 configuration dictionary.
    :type config_dict: dict
    """
    for key, value in config_dict.items():
        # Some attributes must not be directly imported.
        if key in EXCLUDES:
            continue
        # These objects need explicit type conversions.
        if key in DATETIME_COLUMNS:
            continue
        # Some attributes from Mailman 2 were renamed in Mailman 3.
        key = NAME_MAPPINGS.get(key, key)
        # Handle the simple case where the key is an attribute of the
        # IMailingList and the types are the same (modulo 8-bit/unicode
        # strings).
        #
        # If the mailing list has a preferred language that isn't registered
        # in the configuration file, hasattr() will swallow the KeyError this
        # raises and return False.  Treat that attribute specially.
        if key == "preferred_language" or hasattr(mlist, key):
            if isinstance(value, bytes):
                value = bytes_to_str(value)
            # Some types require conversion.
            converter = TYPES.get(key)
            if converter is None:
                column = getattr(mlist.__class__, key, None)
                if column is not None and isinstance(column.type, Boolean):
                    converter = bool
            try:
                if converter is not None:
                    value = converter(value)
                setattr(mlist, key, value)
            except (TypeError, KeyError):
                print('Type conversion error for key "{}": {}'.format(key, value), file=sys.stderr)
    for key in DATETIME_COLUMNS:
        try:
            value = datetime.datetime.utcfromtimestamp(config_dict[key])
        except KeyError:
            continue
        if key == "last_post_time":
            setattr(mlist, "last_post_at", value)
            continue
        setattr(mlist, 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(config_dict.get("default_member_moderation", 0)):
        mlist.default_member_action = member_moderation_action_mapping(config_dict.get("member_moderation_action"))
    else:
        mlist.default_member_action = Action.defer
    # 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 config_dict.get("archive"):
        # For maximum safety, if for some strange reason there's no
        # archive_private key, treat the list as having private archives.
        if config_dict.get("archive_private", True):
            mlist.archive_policy = ArchivePolicy.private
        else:
            mlist.archive_policy = ArchivePolicy.public
    else:
        mlist.archive_policy = ArchivePolicy.never
    # Handle ban list.
    ban_manager = IBanManager(mlist)
    for address in config_dict.get("ban_list", []):
        ban_manager.ban(bytes_to_str(address))
    # Handle acceptable aliases.
    acceptable_aliases = config_dict.get("acceptable_aliases", "")
    if isinstance(acceptable_aliases, bytes):
        acceptable_aliases = acceptable_aliases.decode("utf-8")
    if isinstance(acceptable_aliases, str):
        acceptable_aliases = acceptable_aliases.splitlines()
    alias_set = IAcceptableAliasSet(mlist)
    for address in acceptable_aliases:
        address = address.strip()
        if len(address) == 0:
            continue
        address = bytes_to_str(address)
        try:
            alias_set.add(address)
        except ValueError:
            # When .add() rejects this, the line probably contains a regular
            # expression.  Make that explicit for MM3.
            alias_set.add("^" + address)
    # Handle conversion to URIs.  In MM2.1, the decorations are strings
    # containing placeholders, and there's no provision for language-specific
    # templates.  In MM3, template locations are specified by URLs with the
    # special `mailman:` scheme indicating a file system path.  What we do
    # here is look to see if the list's decoration is different than the
    # default, and if so, we'll write the new decoration template to a
    # `mailman:` scheme path.
    convert_to_uri = {
        "welcome_msg": "welcome_message_uri",
        "goodbye_msg": "goodbye_message_uri",
        "msg_header": "header_uri",
        "msg_footer": "footer_uri",
        "digest_header": "digest_header_uri",
        "digest_footer": "digest_footer_uri",
    }
    # The best we can do is convert only the most common ones.  These are
    # order dependent; the longer substitution with the common prefix must
    # show up earlier.
    convert_placeholders = [
        ("%(real_name)s@%(host_name)s", "$fqdn_listname"),
        ("%(real_name)s", "$display_name"),
        ("%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s", "$listinfo_uri"),
    ]
    # Collect defaults.
    defaults = {}
    for oldvar, newvar in convert_to_uri.items():
        default_value = getattr(mlist, newvar, None)
        if not default_value:
            continue
        # Check if the value changed from the default.
        try:
            default_text = decorate(mlist, default_value)
        except (URLError, KeyError):
            # Use case: importing the old [email protected] into [email protected].  We can't
            # check if it changed from the default so don't import, we may do
            # more harm than good and it's easy to change if needed.
            # TESTME
            print(
                "Unable to convert mailing list attribute:",
                oldvar,
                'with old value "{}"'.format(default_value),
                file=sys.stderr,
            )
            continue
        defaults[newvar] = (default_value, default_text)
    for oldvar, newvar in convert_to_uri.items():
        if oldvar not in config_dict:
            continue
        text = config_dict[oldvar]
        if isinstance(text, bytes):
            text = text.decode("utf-8", "replace")
        for oldph, newph in convert_placeholders:
            text = text.replace(oldph, newph)
        default_value, default_text = defaults.get(newvar, (None, None))
        if not text and not (default_value or default_text):
            # Both are empty, leave it.
            continue
        # Check if the value changed from the default
        try:
            expanded_text = decorate_template(mlist, text)
        except KeyError:
            # Use case: importing the old [email protected] into [email protected]
            # We can't check if it changed from the default
            # -> don't import, we may do more harm than good and it's easy to
            # change if needed
            # TESTME
            print("Unable to convert mailing list attribute:", oldvar, 'with value "{}"'.format(text), file=sys.stderr)
            continue
        if expanded_text and default_text and expanded_text.strip() == default_text.strip():
            # Keep the default.
            continue
        # Write the custom value to the right file.
        base_uri = "mailman:///$listname/$language/"
        if default_value:
            filename = default_value.rpartition("/")[2]
        else:
            filename = "{}.txt".format(newvar[:-4])
        if not default_value or not default_value.startswith(base_uri):
            setattr(mlist, newvar, base_uri + filename)
        filepath = list(search(filename, mlist))[0]
        makedirs(os.path.dirname(filepath))
        with codecs.open(filepath, "w", encoding="utf-8") as fp:
            fp.write(text)
    # Import rosters.
    regulars_set = set(config_dict.get("members", {}))
    digesters_set = set(config_dict.get("digest_members", {}))
    members = regulars_set.union(digesters_set)
    # Don't send welcome messages when we import the rosters.
    send_welcome_message = mlist.send_welcome_message
    mlist.send_welcome_message = False
    try:
        import_roster(mlist, config_dict, members, MemberRole.member)
        import_roster(mlist, config_dict, config_dict.get("owner", []), MemberRole.owner)
        import_roster(mlist, config_dict, config_dict.get("moderator", []), MemberRole.moderator)
        # Now import the '*_these_nonmembers' properties, filtering out the
        # regexps which will remain in the property.
        for action_name in ("accept", "hold", "reject", "discard"):
            prop_name = "{}_these_nonmembers".format(action_name)
            emails = [addr for addr in config_dict.get(prop_name, []) if not addr.startswith("^")]
            import_roster(mlist, config_dict, emails, MemberRole.nonmember, Action[action_name])
            # Only keep the regexes in the legacy list property.
            list_prop = getattr(mlist, prop_name)
            for email in emails:
                list_prop.remove(email)
    finally:
        mlist.send_welcome_message = send_welcome_message
Ejemplo n.º 16
0
def import_config_pck(mlist, config_dict):
    """Apply a config.pck configuration dictionary to a mailing list.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param config_dict: The Mailman 2.1 configuration dictionary.
    :type config_dict: dict
    """
    for key, value in config_dict.items():
        # Some attributes must not be directly imported.
        if key in EXCLUDES:
            continue
        # These objects need explicit type conversions.
        if key in DATETIME_COLUMNS:
            continue
        # Some attributes from Mailman 2 were renamed in Mailman 3.
        key = NAME_MAPPINGS.get(key, key)
        # Handle the simple case where the key is an attribute of the
        # IMailingList and the types are the same (modulo 8-bit/unicode
        # strings).
        #
        # If the mailing list has a preferred language that isn't registered
        # in the configuration file, hasattr() will swallow the KeyError this
        # raises and return False.  Treat that attribute specially.
        if hasattr(mlist, key) or key == 'preferred_language':
            if isinstance(value, str):
                value = str_to_unicode(value)
            # Some types require conversion.
            converter = TYPES.get(key)
            try:
                if converter is not None:
                    value = converter(value)
                setattr(mlist, key, value)
            except (TypeError, KeyError):
                print('Type conversion error for key "{}": {}'.format(
                    key, value), file=sys.stderr)
    for key in DATETIME_COLUMNS:
        try:
            value = datetime.datetime.utcfromtimestamp(config_dict[key])
        except KeyError:
            continue
        if key == 'last_post_time':
            setattr(mlist, 'last_post_at', value)
            continue
        setattr(mlist, key, value)
    # 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 config_dict.get('archive'):
        # For maximum safety, if for some strange reason there's no
        # archive_private key, treat the list as having private archives.
        if config_dict.get('archive_private', True):
            mlist.archive_policy = ArchivePolicy.private
        else:
            mlist.archive_policy = ArchivePolicy.public
    else:
        mlist.archive_policy = ArchivePolicy.never
    # Handle ban list.
    ban_manager = IBanManager(mlist)
    for address in config_dict.get('ban_list', []):
        ban_manager.ban(str_to_unicode(address))
    # Handle acceptable aliases.
    acceptable_aliases = config_dict.get('acceptable_aliases', '')
    if isinstance(acceptable_aliases, basestring):
        acceptable_aliases = acceptable_aliases.splitlines()
    alias_set = IAcceptableAliasSet(mlist)
    for address in acceptable_aliases:
        address = address.strip()
        if len(address) == 0:
            continue
        address = str_to_unicode(address)
        try:
            alias_set.add(address)
        except ValueError:
            # When .add() rejects this, the line probably contains a regular
            # expression.  Make that explicit for MM3.
            alias_set.add('^' + address)
    # Handle conversion to URIs.  In MM2.1, the decorations are strings
    # containing placeholders, and there's no provision for language-specific
    # templates.  In MM3, template locations are specified by URLs with the
    # special `mailman:` scheme indicating a file system path.  What we do
    # here is look to see if the list's decoration is different than the
    # default, and if so, we'll write the new decoration template to a
    # `mailman:` scheme path.
    convert_to_uri = {
        'welcome_msg': 'welcome_message_uri',
        'goodbye_msg': 'goodbye_message_uri',
        'msg_header': 'header_uri',
        'msg_footer': 'footer_uri',
        'digest_header': 'digest_header_uri',
        'digest_footer': 'digest_footer_uri',
        }
    # The best we can do is convert only the most common ones.  These are
    # order dependent; the longer substitution with the common prefix must
    # show up earlier.
    convert_placeholders = [
        ('%(real_name)s@%(host_name)s', '$fqdn_listname'),
        ('%(real_name)s', '$display_name'),
        ('%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s',
         '$listinfo_uri'),
        ]
    # Collect defaults.
    defaults = {}
    for oldvar, newvar in convert_to_uri.items():
        default_value = getattr(mlist, newvar, None)
        if not default_value:
            continue
        # Check if the value changed from the default.
        try:
            default_text = decorate(mlist, default_value)
        except (URLError, KeyError):
            # Use case: importing the old [email protected] into [email protected].  We can't
            # check if it changed from the default so don't import, we may do
            # more harm than good and it's easy to change if needed.
            # TESTME
            print('Unable to convert mailing list attribute:', oldvar,
                  'with old value "{}"'.format(default_value),
                  file=sys.stderr)
            continue
        defaults[newvar] = (default_value, default_text)
    for oldvar, newvar in convert_to_uri.items():
        if oldvar not in config_dict:
            continue
        text = config_dict[oldvar]
        text = text.decode('utf-8', 'replace')
        for oldph, newph in convert_placeholders:
            text = text.replace(oldph, newph)
        default_value, default_text  = defaults.get(newvar, (None, None))
        if not text and not (default_value or default_text):
            # Both are empty, leave it.
            continue
        # Check if the value changed from the default
        try:
            expanded_text = decorate_template(mlist, text)
        except KeyError:
            # Use case: importing the old [email protected] into [email protected]
            # We can't check if it changed from the default
            # -> don't import, we may do more harm than good and it's easy to
            # change if needed
            # TESTME
            print('Unable to convert mailing list attribute:', oldvar,
                  'with value "{}"'.format(text),
                  file=sys.stderr)
            continue
        if (expanded_text and default_text
                and expanded_text.strip() == default_text.strip()):
            # Keep the default.
            continue
        # Write the custom value to the right file.
        base_uri = 'mailman:///$listname/$language/'
        if default_value:
            filename = default_value.rpartition('/')[2]
        else:
            filename = '{}.txt'.format(newvar[:-4])
        if not default_value or not default_value.startswith(base_uri):
            setattr(mlist, newvar, base_uri + filename)
        filepath = list(search(filename, mlist))[0]
        makedirs(os.path.dirname(filepath))
        with codecs.open(filepath, 'w', encoding='utf-8') as fp:
            fp.write(text)
    # Import rosters.
    members = set(config_dict.get('members', {}).keys()
                + config_dict.get('digest_members', {}).keys())
    import_roster(mlist, config_dict, members, MemberRole.member)
    import_roster(mlist, config_dict, config_dict.get('owner', []),
                  MemberRole.owner)
    import_roster(mlist, config_dict, config_dict.get('moderator', []),
                  MemberRole.moderator)
Ejemplo n.º 17
0
def import_config_pck(mlist, config_dict):
    """Apply a config.pck configuration dictionary to a mailing list.

    :param mlist: The mailing list.
    :type mlist: IMailingList
    :param config_dict: The Mailman 2.1 configuration dictionary.
    :type config_dict: dict
    """
    global key
    for key, value in config_dict.items():
        # Some attributes must not be directly imported.
        if key in EXCLUDES:
            continue
        # These objects need explicit type conversions.
        if key in DATETIME_COLUMNS:
            continue
        # Some attributes from Mailman 2 were renamed in Mailman 3.
        key = NAME_MAPPINGS.get(key, key)
        # Handle the simple case where the key is an attribute of the
        # IMailingList and the types are the same (modulo 8-bit/unicode
        # strings).
        #
        # If the mailing list has a preferred language that isn't registered
        # in the configuration file, hasattr() will swallow the KeyError this
        # raises and return False.  Treat that attribute specially.
        if key == 'preferred_language' or hasattr(mlist, key):
            if isinstance(value, bytes):
                value = bytes_to_str(value)
            # Some types require conversion.
            converter = TYPES.get(key)
            if converter is None:
                column = getattr(mlist.__class__, key, None)
                if column is not None and isinstance(column.type, Boolean):
                    converter = bool
                if column is not None and isinstance(column.type, SAUnicode):
                    converter = maybe_truncate_mysql
            try:
                if converter is not None:
                    value = converter(value)
                setattr(mlist, key, value)
            except (TypeError, KeyError):
                print('Type conversion error for key "{}": {}'.format(
                    key, value),
                      file=sys.stderr)
    for key in DATETIME_COLUMNS:
        try:
            value = datetime.datetime.utcfromtimestamp(config_dict[key])
        except KeyError:
            continue
        if key == 'last_post_time':
            setattr(mlist, 'last_post_at', value)
            continue
        setattr(mlist, 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(config_dict.get('default_member_moderation', 0)):
        mlist.default_member_action = member_moderation_action_mapping(
            config_dict.get('member_moderation_action'))
    else:
        mlist.default_member_action = Action.defer
    # 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 (config_dict.get('from_is_list', 0) > config_dict.get(
            'dmarc_moderation_action', 0)):
        mlist.dmarc_mitigate_action = dmarc_action_mapping(
            config_dict.get('from_is_list', 0))
        mlist.dmarc_mitigate_unconditionally = True
    else:
        mlist.dmarc_mitigate_action = dmarc_action_mapping(
            config_dict.get('dmarc_moderation_action', 0))
        mlist.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 config_dict.get('archive'):
        # For maximum safety, if for some strange reason there's no
        # archive_private key, treat the list as having private archives.
        if config_dict.get('archive_private', True):
            mlist.archive_policy = ArchivePolicy.private
        else:
            mlist.archive_policy = ArchivePolicy.public
    else:
        mlist.archive_policy = ArchivePolicy.never
    # Handle ban list.
    ban_manager = IBanManager(mlist)
    for address in config_dict.get('ban_list', []):
        ban_manager.ban(bytes_to_str(address))
    # Handle acceptable aliases.
    acceptable_aliases = config_dict.get('acceptable_aliases', '')
    if isinstance(acceptable_aliases, bytes):
        acceptable_aliases = acceptable_aliases.decode('utf-8')
    if isinstance(acceptable_aliases, str):
        acceptable_aliases = acceptable_aliases.splitlines()
    alias_set = IAcceptableAliasSet(mlist)
    for address in acceptable_aliases:
        address = address.strip()
        if len(address) == 0:
            continue
        address = bytes_to_str(address)
        # All 2.1 acceptable aliases are regexps whether or not they start
        # with '^' or contain '@'.
        if not address.startswith('^'):
            address = '^' + address
        # This used to be in a try which would catch ValueError and add a '^',
        # but .add() would not raise ValueError if address contained '@' and
        # that needs the '^' too as it could be a regexp with an '@' in it.
        alias_set.add(address)
    # Handle roster visibility.
    mapping = member_roster_visibility_mapping(
        config_dict.get('private_roster', None))
    if mapping is not None:
        mlist.member_roster_visibility = mapping
    # Handle header_filter_rules conversion to header_matches.
    header_matches = IHeaderMatchList(mlist)
    header_filter_rules = config_dict.get('header_filter_rules', [])
    for line_patterns, action, _unused in header_filter_rules:
        try:
            chain = action_to_chain(action)
        except KeyError:
            log.warning('Unsupported header_filter_rules action: %r', action)
            continue
        # Now split the line into a header and a pattern.
        for line_pattern in line_patterns.splitlines():
            if len(line_pattern.strip()) == 0:
                continue
            for sep in (': ', ':.*', ':.', ':'):
                header, sep, pattern = line_pattern.partition(sep)
                if sep:
                    # We found it.
                    break
            else:
                # Matches any header, which is not supported.  XXX
                log.warning('Unsupported header_filter_rules pattern: %r',
                            line_pattern)
                continue
            header = header.strip().lstrip('^').lower()
            header = header.replace('\\', '')
            if not header:
                log.warning(
                    'Cannot parse the header in header_filter_rule: %r',
                    line_pattern)
                continue
            if len(pattern) == 0:
                # The line matched only the header, therefore the header can
                # be anything.
                pattern = '.*'
            try:
                re.compile(pattern)
            except re.error:
                log.warning(
                    'Skipping header_filter rule because of an '
                    'invalid regular expression: %r', line_pattern)
                continue
            try:
                header_matches.append(header, pattern, chain)
            except ValueError:
                log.warning('Skipping duplicate header_filter rule: %r',
                            line_pattern)
                continue
    # Handle conversion to URIs.  In MM2.1, the decorations are strings
    # containing placeholders, and there's no provision for language-specific
    # strings.  In MM3, template locations are specified by URLs with the
    # special `mailman:` scheme indicating a file system path.  What we do
    # here is look to see if the list's decoration is different than the
    # default, and if so, we'll write the new decoration template to a
    # `mailman:` scheme path, then add the template to the template manager.
    # We are intentionally omitting the 2.1 welcome_msg here because the
    # string is actually interpolated into a larger template and there's
    # no good way to figure where in the default template to insert it.
    convert_to_uri = {
        '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',
    }
    # The best we can do is convert only the most common ones.  These are
    # order dependent; the longer substitution with the common prefix must
    # show up earlier.
    convert_placeholders = [
        # First convert \r\n that may have been set by a browser to \n.
        ('\r\n', '\n'),
        ('%(real_name)s@%(host_name)s',
         'To unsubscribe send an email to ${short_listname}-leave@${domain}'),
        ('%(real_name)s mailing list',
         '$display_name mailing list -- $listname'),
        # The generic footers no longer have URLs in them.
        ('%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s\n', ''),
    ]
    # Collect defaults.
    manager = getUtility(ITemplateManager)
    defaults = {}
    for oldvar, newvar in convert_to_uri.items():
        default_value = getUtility(ITemplateLoader).get(newvar, mlist)
        if not default_value:
            continue
        # Get the decorated default text
        try:
            default_text = decorate_template(mlist, default_value)
        except (URLError, KeyError):  # pragma: nocover
            # Use case: importing the old [email protected] into [email protected].  We can't
            # check if it changed from the default so don't import, we may do
            # more harm than good and it's easy to change if needed.
            # TESTME
            print('Unable to convert mailing list attribute:',
                  oldvar,
                  'with old value "{}"'.format(default_value),
                  file=sys.stderr)
            continue
        defaults[newvar] = default_text
    for oldvar, newvar in convert_to_uri.items():
        if oldvar not in config_dict:
            continue
        text = config_dict[oldvar]
        if isinstance(text, bytes):
            text = text.decode('utf-8', 'replace')
        for oldph, newph in convert_placeholders:
            text = text.replace(oldph, newph)
        default_text = defaults.get(newvar, None)
        if not text and not default_text:
            # Both are empty, leave it.
            continue
        # Check if the value changed from the default
        try:
            expanded_text = decorate_template(mlist, text)
        except KeyError:  # pragma: nocover
            # Use case: importing the old [email protected] into [email protected]
            # We can't check if it changed from the default
            # -> don't import, we may do more harm than good and it's easy to
            # change if needed
            # TESTME
            print('Unable to convert mailing list attribute:',
                  oldvar,
                  'with value "{}"'.format(text),
                  file=sys.stderr)
            continue
        if (expanded_text and default_text
                and expanded_text.strip() == default_text.strip()):
            # Keep the default.
            continue
        # Write the custom value to the right file and add it to the template
        # manager for real.
        base_uri = 'mailman:///$listname/$language/'
        filename = '{}.txt'.format(newvar)
        manager.set(newvar, mlist.list_id, base_uri + filename)
        with ExitStack() as resources:
            filepath = list(search(resources, filename, mlist))[0]
        makedirs(os.path.dirname(filepath))
        with open(filepath, 'w', encoding='utf-8') as fp:
            fp.write(text)
    # Import rosters.
    regulars_set = set(config_dict.get('members', {}))
    digesters_set = set(config_dict.get('digest_members', {}))
    members = regulars_set.union(digesters_set)
    # Don't send welcome messages or notify admins when we import the rosters.
    send_welcome_message = mlist.send_welcome_message
    mlist.send_welcome_message = False
    admin_notify_mchanges = mlist.admin_notify_mchanges
    mlist.admin_notify_mchanges = False
    try:
        import_roster(mlist, config_dict, members, MemberRole.member)
        import_roster(mlist, config_dict, config_dict.get('owner', []),
                      MemberRole.owner)
        import_roster(mlist, config_dict, config_dict.get('moderator', []),
                      MemberRole.moderator)
        # Now import the '*_these_nonmembers' properties, filtering out the
        # regexps which will remain in the property.
        for action_name in ('accept', 'hold', 'reject', 'discard'):
            prop_name = '{}_these_nonmembers'.format(action_name)
            emails = [
                addr for addr in config_dict.get(prop_name, [])
                if not addr.startswith('^')
            ]
            # MM 2.1 accept maps to MM 3 defer
            if action_name == 'accept':
                action_name = 'defer'
            import_roster(mlist, config_dict, emails, MemberRole.nonmember,
                          Action[action_name])
            # Now add the regexes in the legacy list property.
            list_prop = getattr(mlist, prop_name)
            for addr in config_dict.get(prop_name, []):
                if addr.startswith('^'):
                    list_prop.append(addr)
    finally:
        mlist.send_welcome_message = send_welcome_message
        mlist.admin_notify_mchanges = admin_notify_mchanges
Ejemplo n.º 18
0
 def ensure_directories_exist(self):
     """Create all path directories if they do not exist."""
     if self.create_paths:
         for variable, directory in self.paths.items():
             makedirs(directory)