def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" email = msg.sender if not email: print(_('$self.name: No valid email address found to unsubscribe'), file=results) return ContinueProcessing.no user_manager = getUtility(IUserManager) user = user_manager.get_user(email) if user is None: print(_('No registered user for email address: $email'), file=results) return ContinueProcessing.no # The address that the -leave command was sent from, must be verified. # Otherwise you could link a bogus address to anyone's account, and # then send a leave command from that address. if user_manager.get_address(email).verified_on is None: print(_('Invalid or unverified email address: $email'), file=results) return ContinueProcessing.no already_left = msgdata.setdefault('leaves', set()) for user_address in user.addresses: # Only recognize verified addresses. if user_address.verified_on is None: continue member = mlist.members.get_member(user_address.email) if member is not None: break else: # There are two possible situations. Either none of the user's # addresses are subscribed to this mailing list, or this command # email *already* unsubscribed the user from the mailing list. # E.g. if a message was sent to the -leave address and it # contained the 'leave' command. Don't send a bogus response in # this case, just ignore subsequent leaves of the same address. if email not in already_left: print(_('$self.name: $email is not a member of ' '$mlist.fqdn_listname'), file=results) return ContinueProcessing.no if email in already_left: return ContinueProcessing.yes # Ignore any subsequent 'leave' commands. already_left.add(email) manager = ISubscriptionManager(mlist) token, token_owner, member = manager.unregister(user_address) person = formataddr((user.display_name, email)) # noqa if member is None: print(_('$person left $mlist.fqdn_listname'), file=results) else: print(_('Confirmation email sent to $person to leave' ' $mlist.fqdn_listname'), file=results) return ContinueProcessing.yes
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()
def on_delete(self, request, response): """Delete the member (i.e. unsubscribe).""" # Leaving a list is a bit different than deleting a moderator or # owner. Handle the former case first. For now too, we will not send # an admin or user notification. if self._member is None: not_found(response) return mlist = getUtility(IListManager).get_by_list_id(self._member.list_id) if self._member.role is MemberRole.member: try: values = Validator( pre_confirmed=as_boolean, pre_approved=as_boolean, _optional=('pre_confirmed', 'pre_approved'), )(request) except ValueError as error: bad_request(response, str(error)) return manager = ISubscriptionManager(mlist) # XXX(maxking): For backwards compat, we are going to keep # pre-confirmed to be "True" by defualt instead of "False", that it # should be. Any, un-authenticated requests should manually specify # that it is *not* confirmed by the user. if 'pre_confirmed' in values: pre_confirmed = values.get('pre_confirmed') else: pre_confirmed = True token, token_owner, member = manager.unregister( self._member.address, pre_approved=values.get('pre_approved'), pre_confirmed=pre_confirmed) if member is None: assert token is None assert token_owner is TokenOwner.no_one no_content(response) else: assert token is not None content = dict(token=token, token_owner=token_owner.name) accepted(response, etag(content)) else: self._member.unsubscribe() no_content(response)
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')