class TestUnsubscription(unittest.TestCase): """Test unsubscription requests.""" layer = SMTPLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._manager = ISubscriptionManager(self._mlist) def test_unsubscribe_defer(self): # When unsubscriptions must be approved by the moderator, but the # moderator defers this decision. anne = getUtility(IUserManager).create_address( '*****@*****.**', 'Anne Person') token, token_owner, member = self._manager.register( anne, pre_verified=True, pre_confirmed=True, pre_approved=True) self.assertIsNone(token) self.assertEqual(member.address.email, '*****@*****.**') # Now hold and handle an unsubscription request. token = hold_unsubscription(self._mlist, '*****@*****.**') handle_unsubscription(self._mlist, token, Action.defer) def test_bogus_token(self): # Try to handle an unsubscription with a bogus token. self.assertRaises(LookupError, self._manager.confirm, None)
def setUp(self): self._commandq = config.switchboards['command'] self._runner = make_testable_runner(CommandRunner, 'command') with transaction(): # Register a subscription requiring confirmation. self._mlist = create_list('*****@*****.**') self._mlist.send_welcome_message = False anne = getUtility(IUserManager).create_address('*****@*****.**') registrar = ISubscriptionManager(self._mlist) self._token, token_owner, member = registrar.register(anne)
def add_members(mlist, in_fp, delivery, invite, welcome_msg): """Add members to a mailing list.""" user_manager = getUtility(IUserManager) registrar = ISubscriptionManager(mlist) email_validator = getUtility(IEmailValidator) for line in in_fp: # Ignore blank lines and lines that start with a '#'. if line.startswith('#') or len(line.strip()) == 0: continue # Parse the line and ensure that the values are unicodes. display_name, email = parseaddr(line) # parseaddr can return invalid emails. E.g. parseaddr('foobar@') # returns ('', 'foobar@') in python 3.6.7 and 3.7.1 so check validity. if not email_validator.is_valid(email): line = line.strip() print(_('Cannot parse as valid email address (skipping): $line'), file=sys.stderr) continue subscriber = get_addr(display_name, email, user_manager) # For error messages. email = formataddr((display_name, email)) delivery_status = DeliveryStatus.enabled if delivery is None or delivery == 'regular' or delivery == 'disabled': delivery_mode = DeliveryMode.regular if delivery == 'disabled': delivery_status = DeliveryStatus.by_moderator elif delivery == 'mime': delivery_mode = DeliveryMode.mime_digests elif delivery == 'plain': delivery_mode = DeliveryMode.plaintext_digests elif delivery == 'summary': delivery_mode = DeliveryMode.summary_digests try: member = registrar.register(subscriber, pre_verified=True, pre_approved=True, pre_confirmed=True, invitation=invite, send_welcome_message=welcome_msg)[2] if member is not None: member.preferences.delivery_status = delivery_status member.preferences.delivery_mode = delivery_mode except AlreadySubscribedError: # It's okay if the address is already subscribed, just print a # warning and continue. print(_('Already subscribed (skipping): $email'), file=sys.stderr) except MembershipIsBannedError: print(_('Membership is banned (skipping): $email'), file=sys.stderr) except SubscriptionPendingError: print(_('Subscription already pending (skipping): $email'), file=sys.stderr)
def setUp(self): self._mlist = create_list('*****@*****.**') self._mlist2 = create_list('*****@*****.**') self._mlist.subscription_policy = SubscriptionPolicy.moderate self._mlist.unsubscription_policy = SubscriptionPolicy.moderate msg = mfs("""\ To: [email protected] From: [email protected] Subject: message 1 """) # Hold this message. hold_message(self._mlist, msg, {}, 'Non-member post') # And a second one too. msg2 = mfs("""\ To: [email protected] From: [email protected] Subject: message 2 """) hold_message(self._mlist, msg2, {}, 'Some other reason') usermanager = getUtility(IUserManager) submanager = ISubscriptionManager(self._mlist) # Generate held subscription. usera = usermanager.make_user('*****@*****.**') usera.addresses[0].verified_on = usera.addresses[0].registered_on usera.preferred_address = usera.addresses[0] submanager.register(usera) # Generate a held unsubscription. userb = usermanager.make_user('*****@*****.**') userb.addresses[0].verified_on = userb.addresses[0].registered_on userb.preferred_address = userb.addresses[0] submanager.register(userb, pre_verified=True, pre_confirmed=True, pre_approved=True) submanager.unregister(userb) self._command = CliRunner()
class TestUnsubscription(unittest.TestCase): """Test unsubscription requests.""" layer = SMTPLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._manager = ISubscriptionManager(self._mlist) def test_unsubscribe_defer(self): # When unsubscriptions must be approved by the moderator, but the # moderator defers this decision. user_manager = getUtility(IUserManager) anne = user_manager.create_address('*****@*****.**', 'Anne Person') token, token_owner, member = self._manager.register(anne, pre_verified=True, pre_confirmed=True, pre_approved=True) self.assertIsNone(token) self.assertEqual(member.address.email, '*****@*****.**') bart = user_manager.create_user('*****@*****.**', 'Bart User') address = set_preferred(bart) self._mlist.subscribe(address, MemberRole.moderator) # Now hold and handle an unsubscription request. token = hold_unsubscription(self._mlist, '*****@*****.**') handle_unsubscription(self._mlist, token, Action.defer) items = get_queue_messages('virgin', expected_count=2) # Find the moderator message. for item in items: if item.msg['to'] == '*****@*****.**': break else: raise AssertionError('No moderator email found') self.assertEqual(item.msgdata['recipients'], {'*****@*****.**'}) self.assertEqual( item.msg['subject'], 'New unsubscription request from Test by [email protected]') def test_bogus_token(self): # Try to handle an unsubscription with a bogus token. self.assertRaises(LookupError, self._manager.confirm, None)
def test_duplicate_pending_subscription(self): # Issue #199 - a member's subscription is already pending and they try # to subscribe again. registrar = ISubscriptionManager(self._mlist) with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate anne = self._usermanager.create_address('*****@*****.**') token, token_owner, member = registrar.register( anne, pre_verified=True, pre_confirmed=True) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(member) with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com', 'pre_verified': True, 'pre_confirmed': True, }) self.assertEqual(cm.exception.code, 409) self.assertEqual(cm.exception.reason, 'Subscription request already pending')
class TestSubscriptionModeration(unittest.TestCase): layer = RESTLayer maxDiff = None def setUp(self): with transaction(): self._mlist = create_list('*****@*****.**') self._registrar = ISubscriptionManager(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address('*****@*****.**', 'Anne Person') self._bart = manager.make_user('*****@*****.**', 'Bart Person') set_preferred(self._bart) def test_no_such_list(self): # Try to get the requests of a nonexistent list. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests') self.assertEqual(cm.exception.code, 404) def test_no_such_subscription_token(self): # Bad request when the token is not in the database. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests/missing') self.assertEqual(cm.exception.code, 404) def test_bad_subscription_action(self): # POSTing to a held message with a bad action. token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Let's try to handle her request, but with a bogus action. url = 'http://*****:*****@example.com/requests/{}' with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='bogus', )) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.msg, 'Cannot convert parameters: action') def test_list_held_requests(self): # We can view all the held requests. with transaction(): token_1, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token_1) self.assertIsNone(member) token_2, token_owner, member = self._registrar.register(self._bart) self.assertIsNotNone(token_2) self.assertIsNone(member) json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(response.status_code, 200) self.assertEqual(json['total_size'], 2) tokens = set(entry['token'] for entry in json['entries']) self.assertEqual(tokens, {token_1, token_2}) emails = set(entry['email'] for entry in json['entries']) self.assertEqual(emails, {'*****@*****.**', '*****@*****.**'}) def test_view_malformed_held_message(self): # Opening a bad (i.e. bad structure) email and holding it. email_path = resource_filename('mailman.rest.tests.data', 'bad_email.eml') with open(email_path, 'rb') as fp: msg = message_from_binary_file(fp) msg.sender = '*****@*****.**' with transaction(): hold_message(self._mlist, msg) # Now trying to access held messages from REST API should not give # 500 server error if one of the messages can't be parsed properly. json, response = call_api( 'http://*****:*****@example.com/held') self.assertEqual(response.status_code, 200) self.assertEqual(len(json['entries']), 1) self.assertEqual(json['entries'][0]['msg'], 'This message is defective') def test_individual_request(self): # We can view an individual request. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token) self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token)) self.assertEqual(response.status_code, 200) self.assertEqual(json['token'], token) self.assertEqual(json['token_owner'], token_owner.name) self.assertEqual(json['email'], '*****@*****.**') def test_accept(self): # POST to the request to accept it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='accept', )) self.assertEqual(response.status_code, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='accept', )) self.assertEqual(cm.exception.code, 404) def test_accept_already_subscribed(self): # POST to a subscription request, but the user is already subscribed. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Make Anne already a member. self._mlist.subscribe(self._anne) # Accept the pending subscription, which raises an error. url = 'http://*****:*****@example.com' '/requests/bogus', dict(action='accept')) self.assertEqual(cm.exception.code, 404) def test_accept_by_moderator_clears_request_queue(self): # After accepting a message held for moderator approval, there are no # more requests to handle. # # We start with nothing in the queue. json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(json['total_size'], 0) # Anne tries to subscribe to a list that only requests moderator # approval. with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate token, token_owner, member = self._registrar.register( self._anne, pre_verified=True, pre_confirmed=True) # There's now one request in the queue, and it's waiting on moderator # approval. json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(json['total_size'], 1) entry = json['entries'][0] self.assertEqual(entry['token_owner'], 'moderator') self.assertEqual(entry['email'], '*****@*****.**') # The moderator approves the request. url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), {'action': 'accept'}) self.assertEqual(response.status_code, 204) # And now the request queue is empty. json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(json['total_size'], 0) def test_discard(self): # POST to the request to discard it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='discard', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='discard', )) self.assertEqual(cm.exception.code, 404) def test_defer(self): # Defer the decision for some other moderator. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. json, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status_code, 204) # And now we can accept it. json, response = call_api(url.format(token), dict(action='accept', )) self.assertEqual(response.status_code, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='accept', )) self.assertEqual(cm.exception.code, 404) def test_defer_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com' '/requests/bogus', dict(action='defer')) self.assertEqual(cm.exception.code, 404) def test_reject(self): # POST to the request to reject it. This leaves a bounce message in # the virgin queue. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='reject', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='reject', )) self.assertEqual(cm.exception.code, 404) # And the rejection message to Anne is now in the virgin queue. items = get_queue_messages('virgin') self.assertEqual(len(items), 1) message = items[0].msg self.assertEqual(message['From'], '*****@*****.**') self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Request to mailing list "Ant" rejected') def test_reject_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com' '/requests/bogus', dict(action='reject')) self.assertEqual(cm.exception.code, 404) def test_hold_keeps_holding(self): # POST to the request to continue holding it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='hold', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. json, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status_code, 204) def test_subscribe_other_role_with_no_preferred_address(self): with transaction(): cate = getUtility(IUserManager).create_user('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com') 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')
class TestRegistrar(unittest.TestCase): """Test registration.""" layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = ISubscriptionManager(self._mlist) self._pendings = getUtility(IPendings) self._anne = getUtility(IUserManager).create_address( '*****@*****.**') def test_initial_conditions(self): # Registering a subscription request provides a unique token associated # with a pendable, and the owner of the token. self.assertEqual(self._pendings.count, 0) token, token_owner, member = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(member) self.assertEqual(self._pendings.count, 1) record = self._pendings.confirm(token, expunge=False) self.assertEqual(record['list_id'], self._mlist.list_id) self.assertEqual(record['email'], '*****@*****.**') def test_subscribe(self): # Registering a subscription request where no confirmation or # moderation steps are needed, leaves us with no token or owner, since # there's nothing more to do. self._mlist.subscription_policy = SubscriptionPolicy.open self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) # There's nothing to confirm. record = self._pendings.confirm(token, expunge=False) self.assertIsNone(record) def test_no_such_token(self): # Given a token which is not in the database, a LookupError is raised. self._registrar.register(self._anne) self.assertRaises(LookupError, self._registrar.confirm, 'not-a-token') def test_confirm_because_verify(self): # We have a subscription request which requires the user to confirm # (because she does not have a verified address), but not the moderator # to approve. Running the workflow gives us a token. Confirming the # token subscribes the user. self._mlist.subscription_policy = SubscriptionPolicy.open token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_confirm(self): # We have a subscription request which requires the user to confirm # (because of list policy), but not the moderator to approve. Running # the workflow gives us a token. Confirming the token subscribes the # user. self._mlist.subscription_policy = SubscriptionPolicy.confirm self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_moderation(self): # We have a subscription request which requires the moderator to # approve. Running the workflow gives us a token. Confirming the # token subscribes the user. self._mlist.subscription_policy = SubscriptionPolicy.moderate self._anne.verified_on = now() token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription. token, token_owner, rmember = self._registrar.confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_because_confirm_then_moderation(self): # We have a subscription request which requires the user to confirm # (because she does not have a verified address) and the moderator to # approve. Running the workflow gives us a token. Confirming the # token runs the workflow a little farther, but still gives us a # token. Confirming again subscribes the user. self._mlist.subscription_policy = ( SubscriptionPolicy.confirm_then_moderate) self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription, and wait for the moderator to approve # the subscription. She is still not subscribed. new_token, token_owner, rmember = self._registrar.confirm(token) # The new token, used for the moderator to approve the message, is not # the same as the old token. self.assertNotEqual(new_token, token) self.assertIsNotNone(new_token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Confirm once more, this time as the moderator approving the # subscription. Now she's a member. token, token_owner, rmember = self._registrar.confirm(new_token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_confirm_then_moderate_with_different_tokens(self): # Ensure that the confirmation token the user sees when they have to # confirm their subscription is different than the token the moderator # sees when they approve the subscription. This prevents the user # from using a replay attack to subvert moderator approval. self._mlist.subscription_policy = ( SubscriptionPolicy.confirm_then_moderate) self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now confirm the subscription, and wait for the moderator to approve # the subscription. She is still not subscribed. new_token, token_owner, rmember = self._registrar.confirm(token) # The status is not true because the user has not yet been subscribed # to the mailing list. self.assertIsNotNone(new_token) self.assertEqual(token_owner, TokenOwner.moderator) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # The new token is different than the old token. self.assertNotEqual(token, new_token) # Trying to confirm with the old token does not work. self.assertRaises(LookupError, self._registrar.confirm, token) # Confirm once more, this time with the new token, as the moderator # approving the subscription. Now she's a member. done_token, token_owner, rmember = self._registrar.confirm(new_token) # The token is None, signifying that the member has been subscribed. self.assertIsNone(done_token) self.assertEqual(token_owner, TokenOwner.no_one) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertEqual(rmember, member) self.assertEqual(member.address, self._anne) def test_discard_waiting_for_confirmation(self): # While waiting for a user to confirm their subscription, we discard # the workflow. self._mlist.subscription_policy = SubscriptionPolicy.confirm self._anne.verified_on = now() # Runs until subscription confirmation. token, token_owner, rmember = self._registrar.register(self._anne) self.assertIsNotNone(token) self.assertEqual(token_owner, TokenOwner.subscriber) self.assertIsNone(rmember) member = self._mlist.regular_members.get_member('*****@*****.**') self.assertIsNone(member) # Now discard the subscription request. self._registrar.discard(token) # Trying to confirm the token now results in an exception. self.assertRaises(LookupError, self._registrar.confirm, token) def test_admin_notify_mchanges(self): # When a user gets subscribed via the subscription policy workflow, # the list administrators get an email notification. self._mlist.subscription_policy = SubscriptionPolicy.open self._mlist.admin_notify_mchanges = True self._mlist.send_welcome_message = False token, token_owner, member = self._registrar.register( self._anne, pre_verified=True) # Anne is now a member. self.assertEqual(member.address.email, '*****@*****.**') # And there's a notification email waiting for Bart. items = get_queue_messages('virgin', expected_count=1) message = items[0].msg self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Ant subscription notification') self.assertEqual( message.get_payload(), """\ [email protected] has been successfully subscribed to Ant. """) def test_no_admin_notify_mchanges(self): # Even when a user gets subscribed via the subscription policy # workflow, the list administrators won't get an email notification if # they don't want one. self._mlist.subscription_policy = SubscriptionPolicy.open self._mlist.admin_notify_mchanges = False self._mlist.send_welcome_message = False # Bart is an administrator of the mailing list. bart = getUtility(IUserManager).create_address('*****@*****.**', 'Bart Person') self._mlist.subscribe(bart, MemberRole.owner) token, token_owner, member = self._registrar.register( self._anne, pre_verified=True) # Anne is now a member. self.assertEqual(member.address.email, '*****@*****.**') # There's no notification email waiting for Bart. get_queue_messages('virgin', expected_count=0)
def on_post(self, request, response): """Create a new member.""" try: validator = Validator(list_id=str, subscriber=subscriber_validator(self.api), display_name=str, delivery_mode=enum_validator(DeliveryMode), role=enum_validator(MemberRole), pre_verified=bool, pre_confirmed=bool, pre_approved=bool, _optional=('delivery_mode', 'display_name', 'role', 'pre_verified', 'pre_confirmed', 'pre_approved')) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return # Dig the mailing list out of the arguments. list_id = arguments.pop('list_id') mlist = getUtility(IListManager).get_by_list_id(list_id) if mlist is None: bad_request(response, b'No such list') return # Figure out what kind of subscriber is being registered. Either it's # a user via their preferred email address or it's an explicit address. # If it's a UUID, then it must be associated with an existing user. subscriber = arguments.pop('subscriber') user_manager = getUtility(IUserManager) # We use the display name if there is one. display_name = arguments.pop('display_name', '') if isinstance(subscriber, UUID): user = user_manager.get_user_by_id(subscriber) if user is None: bad_request(response, b'No such user') return subscriber = user else: # This must be an email address. See if there's an existing # address object associated with this email. address = user_manager.get_address(subscriber) if address is None: # Create a new address, which of course will not be validated. address = user_manager.create_address(subscriber, display_name) subscriber = address # What role are we subscribing? Regular members go through the # subscription policy workflow while owners, moderators, and # nonmembers go through the legacy API for now. role = arguments.pop('role', MemberRole.member) if role is MemberRole.member: # Get the pre_ flags for the subscription workflow. pre_verified = arguments.pop('pre_verified', False) pre_confirmed = arguments.pop('pre_confirmed', False) pre_approved = arguments.pop('pre_approved', False) # Now we can run the registration process until either the # subscriber is subscribed, or the workflow is paused for # verification, confirmation, or approval. registrar = ISubscriptionManager(mlist) try: token, token_owner, member = registrar.register( subscriber, pre_verified=pre_verified, pre_confirmed=pre_confirmed, pre_approved=pre_approved) except AlreadySubscribedError: conflict(response, b'Member already subscribed') return except MissingPreferredAddressError: bad_request(response, b'User has no preferred address') return except MembershipIsBannedError: bad_request(response, b'Membership is banned') return except SubscriptionPendingError: conflict(response, b'Subscription request already pending') return if token is None: assert token_owner is TokenOwner.no_one, token_owner # The subscription completed. Let's get the resulting member # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = self.api.from_uuid(member.member_id) location = self.api.path_to('members/{}'.format(member_id)) created(response, location) return # The member could not be directly subscribed because there are # some out-of-band steps that need to be completed. E.g. the user # must confirm their subscription or the moderator must approve # it. In this case, an HTTP 202 Accepted is exactly the code that # we should use, and we'll return both the confirmation token and # the "token owner" so the client knows who should confirm it. assert token is not None, token assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member content = dict(token=token, token_owner=token_owner.name) accepted(response, etag(content)) return # 2015-04-15 BAW: We're subscribing some role other than a regular # member. Use the legacy API for this for now. assert role in (MemberRole.owner, MemberRole.moderator, MemberRole.nonmember) # 2015-04-15 BAW: We're limited to using an email address with this # legacy API, so if the subscriber is a user, the user must have a # preferred address, which we'll use, even though it will subscribe # the explicit address. It is an error if the user does not have a # preferred address. # # If the subscriber is an address object, just use that. if IUser.providedBy(subscriber): if subscriber.preferred_address is None: bad_request(response, b'User without preferred address') return email = subscriber.preferred_address.email else: assert IAddress.providedBy(subscriber) email = subscriber.email delivery_mode = arguments.pop('delivery_mode', DeliveryMode.regular) record = RequestRecord(email, display_name, delivery_mode) try: member = add_member(mlist, record, role) except MembershipIsBannedError: bad_request(response, b'Membership is banned') return except AlreadySubscribedError: bad_request( response, '{} is already an {} of {}'.format(email, role.name, mlist.fqdn_listname)) return # The subscription completed. Let's get the resulting member # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. member_id = self.api.from_uuid(member.member_id) location = self.api.path_to('members/{}'.format(member_id)) created(response, location)
class TestSubscriptionModeration(unittest.TestCase): layer = RESTLayer maxDiff = None def setUp(self): with transaction(): self._mlist = create_list('*****@*****.**') self._mlist.unsubscription_policy = SubscriptionPolicy.moderate self._registrar = ISubscriptionManager(self._mlist) manager = getUtility(IUserManager) self._anne = manager.create_address('*****@*****.**', 'Anne Person') self._bart = manager.make_user('*****@*****.**', 'Bart Person') set_preferred(self._bart) def test_no_such_list(self): # Try to get the requests of a nonexistent list. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests') self.assertEqual(cm.exception.code, 404) def test_no_such_subscription_token(self): # Bad request when the token is not in the database. with self.assertRaises(HTTPError) as cm: call_api('http://*****:*****@example.com/' 'requests/missing') self.assertEqual(cm.exception.code, 404) def test_bad_subscription_action(self): # POSTing to a held message with a bad action. token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Let's try to handle her request, but with a bogus action. url = 'http://*****:*****@example.com/requests/{}' with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='bogus', )) self.assertEqual(cm.exception.code, 400) self.assertEqual( cm.exception.msg, 'Invalid Parameter "action": Accepted Values are:' ' hold, reject, discard, accept, defer.') def test_list_held_requests(self): # We can view all the held requests. with transaction(): token_1, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token_1) self.assertIsNone(member) token_2, token_owner, member = self._registrar.register(self._bart) self.assertIsNotNone(token_2) self.assertIsNone(member) json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(response.status_code, 200) self.assertEqual(json['total_size'], 2) tokens = set(entry['token'] for entry in json['entries']) self.assertEqual(tokens, {token_1, token_2}) emails = set(entry['email'] for entry in json['entries']) self.assertEqual(emails, {'*****@*****.**', '*****@*****.**'}) def test_list_held_requests_with_owner(self): with transaction(): token_1, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token_1) self.assertIsNone(member) token_2, token_owner, member = self._registrar.register(self._bart) self.assertIsNotNone(token_2) self.assertIsNone(member) json, response = call_api( 'http://*****:*****@example.com/requests' '?token_owner=moderator') self.assertEqual(response.status_code, 200) self.assertEqual(json['total_size'], 0) json, response = call_api( 'http://*****:*****@example.com/requests' '?token_owner=subscriber') self.assertEqual(response.status_code, 200) self.assertEqual(json['total_size'], 2) def test_list_held_requests_count(self): with transaction(): token_1, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token_1) self.assertIsNone(member) token_2, token_owner, member = self._registrar.register(self._bart) self.assertIsNotNone(token_2) self.assertIsNone(member) json, response = call_api( 'http://*****:*****@example.com/requests' '/count') self.assertEqual(response.status_code, 200) self.assertEqual(json['count'], 2) json, response = call_api( 'http://*****:*****@example.com/requests' '/count?token_owner=moderator') self.assertEqual(response.status_code, 200) self.assertEqual(json['count'], 0) json, response = call_api( 'http://*****:*****@example.com/requests' '/count?token_owner=subscriber') self.assertEqual(response.status_code, 200) self.assertEqual(json['count'], 2) def test_list_held_unsubscription_request(self): with transaction(): # First, subscribe Anne and then trigger an un-subscription. self._mlist.subscribe(self._bart) token, token_owner, member = self._registrar.unregister(self._bart) # Anne's un-subscription request got held. self.assertIsNotNone(token) self.assertIsNotNone(member) json, response = call_api( 'http://*****:*****@example.com/requests' '?request_type=unsubscription') self.assertEqual(response.status_code, 200) self.assertEqual(len(json['entries']), 1) # Individual request can then be fetched. url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token)) self.assertEqual(json['token'], token) self.assertEqual(json['token_owner'], token_owner.name) self.assertEqual(json['email'], '*****@*****.**') self.assertEqual(json['type'], 'unsubscription') # Bart should still be a Member. self.assertIsNotNone( self._mlist.members.get_member('*****@*****.**')) # Now, accept the request. json, response, call_api(url.format(token), dict(action='accept', )) self.assertEqual(response.status_code, 200) # Now, the Member should be un-subscribed. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) def test_unsubscription_request_count(self): with transaction(): # First, subscribe Anne and then trigger an un-subscription. self._mlist.subscribe(self._bart) token, token_owner, member = self._registrar.unregister(self._bart) # Anne's un-subscription request got held. self.assertIsNotNone(token) self.assertIsNotNone(member) json, response = call_api( 'http://*****:*****@example.com/requests/count' '?request_type=unsubscription') self.assertEqual(response.status_code, 200) self.assertEqual(json['count'], 1) def test_individual_request(self): # We can view an individual request. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNotNone(token) self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token)) self.assertEqual(response.status_code, 200) self.assertEqual(json['token'], token) self.assertEqual(json['token_owner'], token_owner.name) self.assertEqual(json['email'], '*****@*****.**') def test_accept(self): # POST to the request to accept it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='accept', )) self.assertEqual(response.status_code, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='accept', )) self.assertEqual(cm.exception.code, 404) def test_accept_already_subscribed(self): # POST to a subscription request, but the user is already subscribed. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Make Anne already a member. self._mlist.subscribe(self._anne) # Accept the pending subscription, which raises an error. url = 'http://*****:*****@example.com' '/requests/bogus', dict(action='accept')) self.assertEqual(cm.exception.code, 404) def test_accept_by_moderator_clears_request_queue(self): # After accepting a message held for moderator approval, there are no # more requests to handle. # # We start with nothing in the queue. json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(json['total_size'], 0) # Anne tries to subscribe to a list that only requests moderator # approval. with transaction(): self._mlist.subscription_policy = SubscriptionPolicy.moderate token, token_owner, member = self._registrar.register( self._anne, pre_verified=True, pre_confirmed=True) # There's now one request in the queue, and it's waiting on moderator # approval. json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(json['total_size'], 1) entry = json['entries'][0] self.assertEqual(entry['token_owner'], 'moderator') self.assertEqual(entry['email'], '*****@*****.**') # The moderator approves the request. url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), {'action': 'accept'}) self.assertEqual(response.status_code, 204) # And now the request queue is empty. json, response = call_api( 'http://*****:*****@example.com/requests') self.assertEqual(json['total_size'], 0) def test_discard(self): # POST to the request to discard it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='discard', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='discard', )) self.assertEqual(cm.exception.code, 404) def test_defer(self): # Defer the decision for some other moderator. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. json, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status_code, 204) # And now we can accept it. json, response = call_api(url.format(token), dict(action='accept', )) self.assertEqual(response.status_code, 204) # Anne is a member. self.assertEqual( self._mlist.members.get_member('*****@*****.**').address, self._anne) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='accept', )) self.assertEqual(cm.exception.code, 404) def test_defer_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com' '/requests/bogus', dict(action='defer')) self.assertEqual(cm.exception.code, 404) def test_reject(self): # POST to the request to reject it. This leaves a bounce message in # the virgin queue. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='reject', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL no longer exists. with self.assertRaises(HTTPError) as cm: call_api(url.format(token), dict(action='reject', )) self.assertEqual(cm.exception.code, 404) # And the rejection message to Anne is now in the virgin queue. items = get_queue_messages('virgin') self.assertEqual(len(items), 1) message = items[0].msg self.assertEqual(message['From'], '*****@*****.**') self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Request to mailing list "Ant" rejected') def test_reject_bad_token(self): # Try to accept a request with a bogus token. with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com' '/requests/bogus', dict(action='reject')) self.assertEqual(cm.exception.code, 404) def test_reject_with_reason(self): # Try to reject a request with an additional comment/reason. # POST to the request to reject it. This leaves a bounce message in # the virgin queue. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' reason = 'You are not authorized!' json, response = call_api(url.format(token), dict(action='reject', reason=reason)) self.assertEqual(response.status_code, 204) # And the rejection message to Anne is now in the virgin queue. items = get_queue_messages('virgin') self.assertEqual(len(items), 1) message = items[0].msg self.assertEqual(message['From'], '*****@*****.**') self.assertEqual(message['To'], '*****@*****.**') self.assertEqual(message['Subject'], 'Request to mailing list "Ant" rejected') self.assertTrue(reason in message.as_string()) def test_hold_keeps_holding(self): # POST to the request to continue holding it. with transaction(): token, token_owner, member = self._registrar.register(self._anne) # Anne's subscription request got held. self.assertIsNone(member) # Clear out the virgin queue, which currently contains the # confirmation message sent to Anne. get_queue_messages('virgin') url = 'http://*****:*****@example.com/requests/{}' json, response = call_api(url.format(token), dict(action='hold', )) self.assertEqual(response.status_code, 204) # Anne is not a member. self.assertIsNone(self._mlist.members.get_member('*****@*****.**')) # The request URL still exists. json, response = call_api(url.format(token), dict(action='defer', )) self.assertEqual(response.status_code, 204) def test_subscribe_other_role_with_no_preferred_address(self): with transaction(): cate = getUtility(IUserManager).create_user('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api( 'http://*****:*****@example.com') 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')