def check(self, mlist, msg, msgdata): """See `IRule`.""" ban_manager = IBanManager(mlist) user_manager = getUtility(IUserManager) # The NonmemberModeration rule misses unconditionally if any of the # senders are banned. for sender in msg.senders: if ban_manager.is_banned(sender): return False # Every sender email must be a member or nonmember directly. If it is # neither, make the email a nonmembers. for sender in msg.senders: if (mlist.members.get_member(sender) is None and mlist.nonmembers.get_member(sender) is None): # noqa # The email must already be registered, since this happens in # the incoming runner itself. address = user_manager.get_address(sender) assert address is not None, ( 'Posting address is not registered: {}'.format(sender)) mlist.subscribe(address, MemberRole.nonmember) # Check to see if any of the sender emails is already a member. If # so, then this rule misses. member = _find_sender_member(mlist, msg) if member is not None: return False # Do nonmember moderation check. for sender in msg.senders: nonmember = mlist.nonmembers.get_member(sender) assert nonmember is not None, ( "sender didn't get subscribed as a nonmember".format(sender)) # Check the '*_these_nonmembers' properties first. XXX These are # legacy attributes from MM2.1; their database type is 'pickle' and # they should eventually get replaced. for action in ('accept', 'hold', 'reject', 'discard'): legacy_attribute_name = '{}_these_nonmembers'.format(action) checklist = getattr(mlist, legacy_attribute_name) for addr in checklist: if ((addr.startswith('^') and re.match(addr, sender)) or addr == sender): # noqa # The reason will get translated at the point of use. reason = 'The sender is in the nonmember {} list' _record_action(msgdata, action, sender, reason.format(action)) return True action = (mlist.default_nonmember_action if nonmember.moderation_action is None else nonmember.moderation_action) if action is Action.defer: # The regular moderation rules apply. return False elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. # # The reason will get translated at the point of use. reason = 'The message is not from a list member' _record_action(msgdata, action.name, sender, reason) return True # The sender must be a member, so this rule does not match. return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # The MemberModeration rule misses unconditionally if any of the # senders are banned. ban_manager = IBanManager(mlist) for sender in msg.senders: if ban_manager.is_banned(sender): return False member = _find_sender_member(mlist, msg) if member is None: return False action = (mlist.default_member_action if member.moderation_action is None else member.moderation_action) if action is Action.defer: # The regular moderation rules apply. return False elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. msgdata['moderation_action'] = action.name msgdata['moderation_sender'] = sender msgdata.setdefault('moderation_reasons', []).append( # This will get translated at the point of use. 'The message comes from a moderated member') return True # The sender is not a member so this rule does not match. return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" ban_manager = IBanManager(mlist) for sender in msg.senders: if ban_manager.is_banned(sender): return True return False
def test_delete_list_does_not_delete_global_bans(self): # Global bans are not deleted when the list is deleted. global_ban_manager = IBanManager(None) global_ban_manager.ban('*****@*****.**') getUtility(IListManager).delete(self._mlist) self.assertEqual([ban.email for ban in global_ban_manager], ['*****@*****.**'])
def check(self, mlist, msg, msgdata): """See `IRule`.""" ban_manager = IBanManager(mlist) user_manager = getUtility(IUserManager) # The NonmemberModeration rule misses unconditionally if any of the # senders are banned. for sender in msg.senders: if ban_manager.is_banned(sender): return False # Every sender email must be a member or nonmember directly. If it is # neither, make the email a nonmembers. for sender in msg.senders: if (mlist.members.get_member(sender) is None and mlist.nonmembers.get_member(sender) is None): # noqa # The email must already be registered, since this happens in # the incoming runner itself. address = user_manager.get_address(sender) assert address is not None, ( 'Posting address is not registered: {}'.format(sender)) mlist.subscribe(address, MemberRole.nonmember) # Check to see if any of the sender emails is already a member. If # so, then this rule misses. member = _find_sender_member(mlist, msg) if member is not None: return False # Do nonmember moderation check. for sender in msg.senders: nonmember = mlist.nonmembers.get_member(sender) assert nonmember is not None, ( "sender didn't get subscribed as a nonmember".format(sender)) # Check the '*_these_nonmembers' properties first. XXX These are # legacy attributes from MM2.1; their database type is 'pickle' and # they should eventually get replaced. for action in ('accept', 'hold', 'reject', 'discard'): legacy_attribute_name = '{}_these_nonmembers'.format(action) checklist = getattr(mlist, legacy_attribute_name) for addr in checklist: if ((addr.startswith('^') and re.match(addr, sender)) or addr == sender): # noqa: W503 # The reason will get translated at the point of use. reason = 'The sender is in the nonmember {} list' _record_action(msgdata, action, sender, reason.format(action)) return True action = (mlist.default_nonmember_action if nonmember.moderation_action is None else nonmember.moderation_action) if action is Action.defer: # The regular moderation rules apply. return False elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. # # The reason will get translated at the point of use. reason = 'The message is not from a list member' _record_action(msgdata, action.name, sender, reason) return True # The sender must be a member, so this rule does not match. return False
def test_subscribe_other_role_banned_email_address(self): bans = IBanManager(self._mlist) with transaction(): bans.ban('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com', 'role': 'moderator', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, b'Membership is banned')
def test_subscribe_other_role_banned_email_address(self): bans = IBanManager(self._mlist) with transaction(): bans.ban('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com', 'role': 'moderator', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'Membership is banned')
def check(self, mlist, msg, msgdata): """See `IRule`.""" ban_manager = IBanManager(mlist) for sender in msg.senders: if ban_manager.is_banned(sender): msgdata['moderation_sender'] = sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( (_('Message sender {} is banned from this list'), sender)) return True return False
def test_not_found_after_unbanning_global(self): manager = IBanManager(None) with transaction(): manager.ban("*****@*****.**") url = "http://*****:*****@example.com" response, content = call_api(url) self.assertEqual(response["email"], "*****@*****.**") response, content = call_api(url, method="DELETE") self.assertEqual(content.status, 204) with self.assertRaises(HTTPError) as cm: call_api(url) self.assertEqual(cm.exception.code, 404) self.assertEqual(cm.exception.reason, b"Email is not banned: [email protected]")
def test_not_found_after_unbanning_global(self): manager = IBanManager(None) with transaction(): manager.ban('*****@*****.**') url = ('http://*****:*****@example.com') response, content = call_api(url) self.assertEqual(response['email'], '*****@*****.**') response, content = call_api(url, method='DELETE') self.assertEqual(content.status, 204) with self.assertRaises(HTTPError) as cm: call_api(url) self.assertEqual(cm.exception.code, 404) self.assertEqual(cm.exception.reason, b'Email is not banned: [email protected]')
def test_not_found_after_unbanning(self): manager = IBanManager(self._mlist) with transaction(): manager.ban('*****@*****.**') url = ('http://*****:*****@example.com') json, response = call_api(url) self.assertEqual(json['email'], '*****@*****.**') json, response = call_api(url, method='DELETE') self.assertEqual(response.status_code, 204) with self.assertRaises(HTTPError) as cm: call_api(url) self.assertEqual(cm.exception.code, 404) self.assertEqual(cm.exception.reason, 'Email is not banned: [email protected]')
def test_add_member_banned_by_pattern(self): # Addresses matching regexp ban patterns cannot subscribe. IBanManager(self._mlist).ban('^.*@example.com') self.assertRaises( MembershipIsBannedError, add_member, self._mlist, '*****@*****.**', 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language)
def test_banned_address_linked_to_user(self): # Anne is subscribed to a mailing list as a user with her preferred # address. She also has a secondary address which is banned and which # she uses to post to the mailing list. Both the MemberModeration and # NonmemberModeration rules miss because the posting address is # banned. user_manager = getUtility(IUserManager) anne = user_manager.create_user('*****@*****.**') set_preferred(anne) self._mlist.subscribe(anne, MemberRole.member) anne.link(user_manager.create_address('*****@*****.**')) IBanManager(self._mlist).ban('*****@*****.**') msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) rule = moderation.MemberModeration() result = rule.check(self._mlist, msg, {}) self.assertFalse(result) rule = moderation.NonmemberModeration() result = rule.check(self._mlist, msg, {}) self.assertFalse(result)
def _step_sanity_checks(self): # Ensure that we have both an address and a user, even if the address # is not verified. We can't set the preferred address until it is # verified. if self.user is None: # The address has no linked user so create one, link it, and set # the user's preferred address. assert self.address is not None, 'No address or user' self.user = getUtility(IUserManager).make_user(self.address.email) if self.address is None: assert self.user.preferred_address is None, ( "Preferred address exists, but wasn't used in constructor") addresses = list(self.user.addresses) if len(addresses) == 0: raise AssertionError('User has no addresses: {}'.format( self.user)) # This is rather arbitrary, but we have no choice. self.address = addresses[0] assert self.user is not None and self.address is not None, ( 'Insane sanity check results') # Is this email address banned? if IBanManager(self.mlist).is_banned(self.address.email): raise MembershipIsBannedError(self.mlist, self.address.email) # Start out with the subscriber being the token owner. self.push('verification_checks')
def test_add_member_globally_banned(self): # Test that members who are banned by specific address cannot # subscribe to the mailing list. IBanManager(None).ban('*****@*****.**') self.assertRaises( MembershipIsBannedError, add_member, self._mlist, '*****@*****.**', 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language)
def test_add_member_banned_from_different_list(self): # Test that members who are banned by on a different list can still be # subscribed to other mlists. sample_list = create_list('*****@*****.**') IBanManager(sample_list).ban('*****@*****.**') member = add_member(self._mlist, '*****@*****.**', 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language) self.assertEqual(member.address.email, '*****@*****.**')
def test_add_member_banned_from_different_list_by_pattern(self): # Addresses matching regexp ban patterns on one list can still # subscribe to other mailing lists. sample_list = create_list('*****@*****.**') IBanManager(sample_list).ban('^.*@example.com') member = add_member(self._mlist, '*****@*****.**', 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language) self.assertEqual(member.address.email, '*****@*****.**')
def test_ban_list(self): banned = [ ('*****@*****.**', '*****@*****.**'), ('^.*@example.com', '*****@*****.**'), ('non-ascii-\[email protected]', 'non-ascii-\[email protected]'), ] self._pckdict['ban_list'] = [b[0].encode('iso-8859-1') for b in banned] self._import() for _pattern, addr in banned: self.assertTrue(IBanManager(self._mlist).is_banned(addr))
def add_member(mlist, record, role=MemberRole.member): """Add a member right now. The member's subscription must be approved by whatever policy the list enforces. :param mlist: The mailing list to add the member to. :type mlist: `IMailingList` :param record: a subscription request record. :type record: RequestRecord :param role: The membership role for this subscription. :type role: `MemberRole` :return: The just created member. :rtype: `IMember` :raises AlreadySubscribedError: if the user is already subscribed to the mailing list. :raises InvalidEmailAddressError: if the email address is not valid. :raises MembershipIsBannedError: if the membership is not allowed. """ # Check to see if the email address is banned. if IBanManager(mlist).is_banned(record.email): raise MembershipIsBannedError(mlist, record.email) # Make sure there is a user linked with the given address. user_manager = getUtility(IUserManager) user = user_manager.make_user(record.email, record.display_name) user.preferences.preferred_language = record.language # Subscribe the address, not the user. # We're looking for two versions of the email address, the case # preserved version and the case insensitive version. We'll # subscribe the version with matching case if it exists, otherwise # we'll use one of the matching case-insensitively ones. It's # undefined which one we pick. case_preserved = None case_insensitive = None for address in user.addresses: if address.original_email == record.email: case_preserved = address if address.email == record.email.lower(): case_insensitive = address assert case_preserved is not None or case_insensitive is not None, ( 'Could not find a linked address for: {}'.format(record.email)) address = (case_preserved if case_preserved is not None else case_insensitive) # Create the member and set the appropriate preferences. It's # possible we're subscribing the lower cased version of the address; # if that's already subscribed re-issue the exception with the correct # email address (i.e. the one passed in here). try: member = mlist.subscribe(address, role) except AlreadySubscribedError as error: raise AlreadySubscribedError(error.fqdn_listname, record.email, error.role) member.preferences.preferred_language = record.language member.preferences.delivery_mode = record.delivery_mode return member
def test_add_member_banned(self): # Test that members who are banned by specific address cannot # subscribe to the mailing list. IBanManager(self._mlist).ban('*****@*****.**') with self.assertRaises(MembershipIsBannedError) as cm: add_member(self._mlist, '*****@*****.**', 'Anne Person', '123', DeliveryMode.regular, system_preferences.preferred_language) self.assertEqual( str(cm.exception), '[email protected] is not allowed to subscribe to [email protected]')
class TestMailingListBans(unittest.TestCase): layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._manager = IBanManager(self._mlist) def test_delete_list(self): # All list bans must be deleted when the list is deleted. self._manager.ban('*****@*****.**') getUtility(IListManager).delete(self._mlist) self.assertEqual(list(self._manager), []) def test_delete_list_does_not_delete_global_bans(self): # Global bans are not deleted when the list is deleted. global_ban_manager = IBanManager(None) global_ban_manager.ban('*****@*****.**') getUtility(IListManager).delete(self._mlist) self.assertEqual([ban.email for ban in global_ban_manager], ['*****@*****.**'])
def test_globally_banned_member_tries_to_join(self): # A user tries to join a list they are banned from. with transaction(): IBanManager(None).ban('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com', }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, 'Membership is banned')
def test_banned_address(self): IBanManager(self._mlist).ban('*****@*****.**') with NamedTemporaryFile('w', buffering=1, encoding='utf-8') as infp: print('Anne Person <*****@*****.**>', file=infp) result = self._command.invoke(addmembers, ( infp.name, 'ant.example.com')) self.assertEqual( result.output, 'Membership is banned (skipping): ' 'Anne Person <*****@*****.**>\n' ) self.assertEqual(len(list(self._mlist.members.members)), 0)
def test_confirm_banned_address(self): # Confirmation of a banned address should return an appropriate error. IBanManager(self._mlist).ban('*****@*****.**') result = Results() status = self._command.process(self._mlist, Message(), {}, (self._token, ), result) self.assertEqual(status, ContinueProcessing.no) # Anne will not be subscribed. self.assertFalse(self._mlist.is_subscribed('*****@*****.**')) # The result will contain an error message. self.assertIn( '[email protected] is not allowed to subscribe to ' '*****@*****.**', str(result))
def test_join_banned(self): # Try to subscribe someone who is banned. Anne is a real # user, with a validated address, but she is not a member of the # mailing list and is banned from joining. # Add anne to the ban list. IBanManager(self._mlist).ban('*****@*****.**') # Then initiate a subscription. msg = Message() msg['From'] = '*****@*****.**' results = Results() self._command.process(self._mlist, msg, {}, (), results) self.assertEqual( str(results).splitlines()[-1], '[email protected] is not allowed to subscribe to [email protected]')
def _step_sanity_checks(self): # Ensure that we have both an address and a user, even if the address # is not verified. We can't set the preferred address until it is # verified. if self.user is None: # The address has no linked user so create one, link it, and set # the user's preferred address. assert self.address is not None, 'No address or user' self.user = getUtility(IUserManager).make_user(self.address.email) if self.address is None: assert self.user.preferred_address is None, ( "Preferred address exists, but wasn't used in constructor") addresses = list(self.user.addresses) if len(addresses) == 0: raise AssertionError('User has no addresses: {}'.format( self.user)) # This is rather arbitrary, but we have no choice. self.address = addresses[0] assert self.user is not None and self.address is not None, ( 'Insane sanity check results') # Is this subscriber already a member? if (self.which is WhichSubscriber.user and self.user.preferred_address is not None): subscriber = self.user else: subscriber = self.address if self.mlist.is_subscribed(subscriber): # 2017-04-22 BAW: This branch actually *does* get covered, as I've # verified by a full coverage run, but diffcov for some reason # claims that the test added in the branch that added this code # does not cover the change. That seems like a bug in diffcov. raise AlreadySubscribedError( # pragma: nocover self.mlist.fqdn_listname, self.address.email, MemberRole.member) # Is this email address banned? if IBanManager(self.mlist).is_banned(self.address.email): raise MembershipIsBannedError(self.mlist, self.address.email) # Don't allow the list posting address. if self.address.email.lower() == self.mlist.posting_address: raise InvalidEmailAddressError('List posting address not allowed') # Check if there is already a subscription request for this email. pendings = getUtility(IPendings).find(mlist=self.mlist, pend_type='subscription') for token, pendable in pendings: if pendable['email'] == self.address.email: raise SubscriptionPendingError(self.mlist, self.address.email) # Start out with the subscriber being the token owner. self.push('verification_checks')
def test_simple_banned_sender(self): # Simple case where the sender is banned. user_manager = getUtility(IUserManager) anne = user_manager.create_user('*****@*****.**') set_preferred(anne) IBanManager(self._mlist).ban('*****@*****.**') msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) rule = banned_address.BannedAddress() result = rule.check(self._mlist, msg, {}) self.assertTrue(result)
def test_banned_sender_among_multiple_senders(self): # Two addresses are created, one of which is banned. The rule matches # because all senders are checked. user_manager = getUtility(IUserManager) user_manager.create_address('*****@*****.**') user_manager.create_address('*****@*****.**') IBanManager(self._mlist).ban('*****@*****.**') msg = mfs("""\ From: [email protected] Sender: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) rule = banned_address.BannedAddress() result = rule.check(self._mlist, msg, {}) self.assertTrue(result)
def subscribe(self, store, subscriber, role=MemberRole.member, send_welcome_message=None): """See `IMailingList`.""" member, email = self._get_subscriber(store, subscriber, role) test_email = email or subscriber.lower() # Allow list posting address only for nonmember role. if (test_email == self.posting_address and role != MemberRole.nonmember): raise InvalidEmailAddressError('List posting address not allowed') if member is not None: raise AlreadySubscribedError(self.fqdn_listname, email, role) if IBanManager(self).is_banned(test_email): raise MembershipIsBannedError(self, test_email) member = Member(role=role, list_id=self._list_id, subscriber=subscriber) member.preferences = Preferences() store.add(member) notify(SubscriptionEvent( self, member, send_welcome_message=send_welcome_message)) return member
def test_banned_sender_among_multiple_senders(self): # Two addresses are created, one of which is banned. Even though the # The Nonmember moderation rule misses if any of the banned addresses # appear in the 'senders' headers of the message. user_manager = getUtility(IUserManager) user_manager.create_address('*****@*****.**') user_manager.create_address('*****@*****.**') IBanManager(self._mlist).ban('*****@*****.**') rule = moderation.NonmemberModeration() msg = mfs("""\ From: [email protected] Sender: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) result = rule.check(self._mlist, msg, {}) self.assertFalse(result)
def test_rule_returns_reason(self): # Ensure a reason is returned. user_manager = getUtility(IUserManager) anne = user_manager.create_user('*****@*****.**') set_preferred(anne) IBanManager(self._mlist).ban('*****@*****.**') msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) rule = banned_address.BannedAddress() msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) self.assertEqual(msgdata['moderation_reasons'], [ ('Message sender {} is banned from this list', '*****@*****.**') ])
def test_banned_address_linked_to_user(self): # Anne is subscribed to a mailing list as a user with her preferred # address. She also has a secondary address which is banned and which # she uses to post to the mailing list. The rule matches because the # posting address is banned. user_manager = getUtility(IUserManager) anne = user_manager.create_user('*****@*****.**') set_preferred(anne) anne.link(user_manager.create_address('*****@*****.**')) IBanManager(self._mlist).ban('*****@*****.**') msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) rule = banned_address.BannedAddress() result = rule.check(self._mlist, msg, {}) self.assertTrue(result)
class TestMailingListBans(unittest.TestCase): layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._manager = IBanManager(self._mlist) def test_delete_list(self): # All list bans must be deleted when the list is deleted. self._manager.ban('*****@*****.**') getUtility(IListManager).delete(self._mlist) self.assertEqual(list(self._manager), []) def test_delete_list_does_not_delete_global_bans(self): # Global bans are not deleted when the list is deleted. global_ban_manager = IBanManager(None) global_ban_manager.ban('*****@*****.**') getUtility(IListManager).delete(self._mlist) self.assertEqual([ban.email for ban in global_ban_manager], ['*****@*****.**']) def test_bans_sequence(self): # Bans returns a pageable sorted sequence. self._manager.ban('*****@*****.**') self._manager.ban('*****@*****.**') self._manager.ban('*****@*****.**') # The results can be len()'d. count = len(self._manager.bans) self.assertEqual(count, 3) # The results can be iterated. self.assertEqual( ['*****@*****.**', '*****@*****.**', '*****@*****.**'], [ban.email for ban in self._manager.bans]) # The results can be indexed. self.assertEqual( [self._manager.bans[i].email for i in range(count)], ['*****@*****.**', '*****@*****.**', '*****@*****.**'])
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
def setUp(self): self._mlist = create_list('*****@*****.**') self._manager = IBanManager(self._mlist)
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)