class IndividualRequest(_ModerationBase): """Resource for moderating a membership change.""" def __init__(self, mlist, token): super().__init__() self._mlist = mlist self._registrar = ISubscriptionManager(self._mlist) self._token = token def on_get(self, request, response): # Get the pended record associated with this token, if it exists in # the pending table. try: resource = self._resource_as_dict(self._token) assert resource is not None, resource except LookupError: not_found(response) return okay(response, etag(resource)) def on_post(self, request, response): try: validator = Validator(action=enum_validator(Action)) arguments = validator(request) except ValueError as error: bad_request(response, str(error)) return action = arguments['action'] if action in (Action.defer, Action.hold): # At least see if the token is in the database. pendable = self._pendings.confirm(self._token, expunge=False) if pendable is None: not_found(response) else: no_content(response) elif action is Action.accept: try: self._registrar.confirm(self._token) except LookupError: not_found(response) except AlreadySubscribedError: conflict(response, 'Already subscribed') else: no_content(response) elif action is Action.discard: # At least see if the token is in the database. pendable = self._pendings.confirm(self._token, expunge=True) if pendable is None: not_found(response) else: no_content(response) else: assert action is Action.reject, action # Like discard but sends a rejection notice to the user. pendable = self._pendings.confirm(self._token, expunge=True) if pendable is None: not_found(response) else: no_content(response) send_rejection(self._mlist, _('Subscription request'), pendable['email'], _('[No reason given]'))
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 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 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 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 test_confirmation_message(self): # Create an address to subscribe. address = getUtility(IUserManager).create_address( '*****@*****.**', 'Anne Person') # Register the address with the list to create a confirmation notice. ISubscriptionManager(self._mlist).register(address) # Now there's one message in the virgin queue. items = get_queue_messages('virgin', expected_count=1) message = items[0].msg self.assertTrue(str(message['subject']).startswith('confirm')) self.assertMultiLineEqual( message.get_payload(), """\ Email Address Registration Confirmation Hello, this is the GNU Mailman server at example.com. We have received a registration request for the email address [email protected] Before you can start using GNU Mailman at this site, you must first confirm that this is your email address. You can do this by replying to this message, keeping the Subject header intact. If you do not wish to register this email address, simply disregard this message. If you think you are being maliciously subscribed to the list, or have any other questions, you may contact [email protected] """)
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._mlist = create_list('*****@*****.**') anne = getUtility(IUserManager).create_address('*****@*****.**', 'Anne Person') self._token, token_owner, member = ISubscriptionManager( self._mlist).register(anne) self._command = Confirm() # Clear the virgin queue. get_queue_messages('virgin')
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)
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')
def syncmembers(ctx, in_fp, delivery, welcome_msg, goodbye_msg, admin_notify, no_change, listspec): """Add and delete mailing list members to match an input file.""" global email_validator, registrar, user_manager mlist = getUtility(IListManager).get(listspec) if mlist is None: ctx.fail(_('No such list: $listspec')) email_validator = getUtility(IEmailValidator) registrar = ISubscriptionManager(mlist) user_manager = getUtility(IUserManager) sync_members(mlist, in_fp, delivery, welcome_msg, goodbye_msg, admin_notify, no_change)
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # Parse the arguments. delivery_mode, address = self._parse_arguments(arguments, results) if delivery_mode is ContinueProcessing.no: return ContinueProcessing.no if not address: # RFC 2047 decode the From: header. if msg['from'] is None: display_name, email = ('', '') else: decoded_from = str(make_header(decode_header(msg['from']))) display_name, email = parseaddr(decoded_from) else: display_name, email = ('', address) # Address could be None or the empty string. if not email: email = msg.sender if not email: print(_('$self.name: No valid address found to subscribe'), file=results) return ContinueProcessing.no if isinstance(email, bytes): email = email.decode('ascii') # Have we already seen one join request from this user during the # processing of this email? joins = getattr(results, 'joins', set()) if email in joins: # Do not register this join. return ContinueProcessing.yes joins.add(email) results.joins = joins person = formataddr((display_name, email)) # noqa: F841 try: subscriber = match_subscriber(email, display_name) except InvalidEmailAddressError as e: print('Invalid email address: {}'.format(e), file=results) return ContinueProcessing.yes try: ISubscriptionManager(mlist).register(subscriber) except (AlreadySubscribedError, InvalidEmailAddressError, MembershipIsBannedError) as e: print(str(e), file=results) except SubscriptionPendingError: # SubscriptionPendingError doesn't return an error message. listname = mlist.fqdn_listname # noqa: F841 print(_('$person has a pending subscription for $listname'), file=results) else: print(_('Confirmation email sent to $person'), file=results) return ContinueProcessing.yes
def test_nonascii_confirmation_message(self): # Add the 'yy' language and set it getUtility(ILanguageManager).add('yy', 'utf-8', 'Ylandia') self._mlist.preferred_language = 'yy' # Create an address to subscribe. address = getUtility(IUserManager).create_address( '*****@*****.**', 'Anne Person') # Register the address with the list to create a confirmation notice. ISubscriptionManager(self._mlist).register(address) # Now there's one message in the virgin queue. items = get_queue_messages('virgin', expected_count=1) message = items[0].msg self.assertTrue(str(message['subject']).startswith('Your confirm')) self.assertMultiLineEqual( message.get_payload(decode=True).decode('utf-8'), 'Wé need your confirmation\n')
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # The token must be in the arguments. if len(arguments) == 0: print(_('No confirmation token found'), file=results) return ContinueProcessing.no # Make sure we don't try to confirm the same token more than once. token = arguments[0] tokens = getattr(results, 'confirms', set()) if token in tokens: # Do not try to confirm this one again. return ContinueProcessing.no tokens.add(token) results.confirms = tokens try: new_token, token_owner, member = ISubscriptionManager( mlist).confirm(token) if new_token is None: assert token_owner is TokenOwner.no_one, token_owner # We can't assert anything about member. It will be None when # the workflow we're confirming is a subscription request, # and non-None when we're confirming an unsubscription request. # This class doesn't know which is happening. succeeded = True elif token_owner is TokenOwner.moderator: # This must have been a confirm-then-moderate (un)subscription. assert new_token != token # We can't assert anything about member for the above reason. succeeded = True else: assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member succeeded = False except LookupError: # The token must not exist in the database. succeeded = False except MembershipIsBannedError as e: print(str(e), file=results) return ContinueProcessing.no if succeeded: print(_('Confirmed'), file=results) # After the 'confirm' command, do not process any other commands in # the email. return ContinueProcessing.no print(_('Confirmation token did not match'), file=results) return ContinueProcessing.no
def test_subscription_pending(self): # Create an address. address = getUtility(IUserManager).create_address( '*****@*****.**', 'Anne Person') # Pend a subscription. self._mlist.subscription_policy = SubscriptionPolicy.confirm ISubscriptionManager(self._mlist).register(address) 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, 'Subscription already pending (skipping): ' 'Anne Person <*****@*****.**>\n' ) self.assertEqual(len(list(self._mlist.members.members)), 0)
def test_join_pending(self): self._mlist.subscription_policy = SubscriptionPolicy.confirm # Try to subscribe someone who already has a subscription pending. # Anne is a real user, with a validated address, who already has a # pending subscription for this mailing list. anne = getUtility(IUserManager).create_user('*****@*****.**') set_preferred(anne) # Initiate a subscription. ISubscriptionManager(self._mlist).register(anne) # And try to subscribe. msg = Message() msg['From'] = '*****@*****.**' results = Results() self._command.process(self._mlist, msg, {}, (), results) self.assertEqual( str(results).splitlines()[-1], '[email protected] has a pending subscription for [email protected]')
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_join_already_a_member(self): # Try to subscribe someone who is already a member. Anne is a real # user, with a validated address, but she is not a member of the # mailing list yet. anne = getUtility(IUserManager).create_user('*****@*****.**') set_preferred(anne) # First subscribe anne. ISubscriptionManager(self._mlist).register(anne, pre_verified=True, pre_confirmed=True, pre_approved=True) # 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 already a MemberRole.member of ' 'mailing list [email protected]')
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 _confirm(self): # There will be two messages in the queue - the confirmation messages, # and a reply to Anne notifying her of the status of her command # email. We need to dig the confirmation token out of the Subject # header of the latter so that we can confirm the subscription. items = get_queue_messages('virgin', sort_on='subject', expected_count=2) subject_words = str(items[1].msg['subject']).split() self.assertEqual(subject_words[0], 'confirm') token = subject_words[1] token, token_owner, rmember = ISubscriptionManager( self._mlist).confirm(token) self.assertIsNone(token) self.assertEqual(token_owner, TokenOwner.no_one) # Now, make sure that Anne is a member of the list and is receiving # digest deliveries. members = getUtility(ISubscriptionService).find_members( '*****@*****.**') self.assertEqual(len(members), 1) self.assertEqual(rmember, members[0]) return rmember
def test_confirmation_message(self): # Create an address to subscribe. address = getUtility(IUserManager).create_address( '*****@*****.**', 'Anne Person') # Register the address with the list to create a confirmation notice. ISubscriptionManager(self._mlist).register(address) # Now there's one message in the virgin queue. items = get_queue_messages('virgin', expected_count=1) message = items[0].msg self.assertTrue(str(message['subject']).startswith('Your confirm')) token = re.sub(r'^.*\+([^+@]*)@.*$', r'\1', str(message['from'])) self.assertMultiLineEqual( message.get_payload(), """\ Email Address Registration Confirmation Hello, this is the GNU Mailman server at example.com. We have received a registration request for the email address [email protected] Before you can start using GNU Mailman at this site, you must first confirm that this is your email address. You can do this by replying to this message. Or you should include the following line -- and only the following line -- in a message to [email protected]: confirm {} Note that simply sending a `reply' to this message should work from most mail readers. If you do not wish to register this email address, simply disregard this message. If you think you are being maliciously subscribed to the list, or have any other questions, you may contact [email protected] """.format(token))
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # The token must be in the arguments. if len(arguments) == 0: print(_('No confirmation token found'), file=results) return ContinueProcessing.no # Make sure we don't try to confirm the same token more than once. token = arguments[0] tokens = getattr(results, 'confirms', set()) if token in tokens: # Do not try to confirm this one again. return ContinueProcessing.yes tokens.add(token) results.confirms = tokens try: new_token, token_owner, member = ISubscriptionManager( mlist).confirm(token) if new_token is None: assert token_owner is TokenOwner.no_one, token_owner assert member is not None, member succeeded = True elif token_owner is TokenOwner.moderator: # This must have been a confirm-then-moderator subscription. assert new_token != token assert member is None, member succeeded = True else: assert token_owner is not TokenOwner.no_one, token_owner assert member is None, member succeeded = False except LookupError: # The token must not exist in the database. succeeded = False if succeeded: print(_('Confirmed'), file=results) return ContinueProcessing.yes print(_('Confirmation token did not match'), file=results) return ContinueProcessing.no
def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" # Parse the arguments. delivery_mode = self._parse_arguments(arguments, results) if delivery_mode is ContinueProcessing.no: return ContinueProcessing.no display_name, email = parseaddr(msg['from']) # Address could be None or the empty string. if not email: email = msg.sender if not email: print(_('$self.name: No valid address found to subscribe'), file=results) return ContinueProcessing.no if isinstance(email, bytes): email = email.decode('ascii') # Have we already seen one join request from this user during the # processing of this email? joins = getattr(results, 'joins', set()) if email in joins: # Do not register this join. return ContinueProcessing.yes joins.add(email) results.joins = joins person = formataddr((display_name, email)) # noqa: F841 # Is this person already a member of the list? Search for all # matching memberships. members = getUtility(ISubscriptionService).find_members( email, mlist.list_id, MemberRole.member) if len(members) > 0: print(_('$person is already a member'), file=results) return ContinueProcessing.yes subscriber = match_subscriber(email, display_name) ISubscriptionManager(mlist).register(subscriber) print(_('Confirmation email sent to $person'), file=results) return ContinueProcessing.yes
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')
def setUp(self): self._mlist = create_list('*****@*****.**') self._registrar = ISubscriptionManager(self._mlist) self._pendings = getUtility(IPendings) self._anne = getUtility(IUserManager).create_address( '*****@*****.**')
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)
def __init__(self, mlist, token): super().__init__() self._mlist = mlist self._registrar = ISubscriptionManager(self._mlist) self._token = token
def test_confirm_then_moderate_workflow(self): # Issue #114 describes a problem when confirming the moderation email. self._mlist.subscription_policy = ( SubscriptionPolicy.confirm_then_moderate) bart = getUtility(IUserManager).create_address('*****@*****.**', 'Bart Person') # Clear any previously queued confirmation messages. get_queue_messages('virgin') self._token, token_owner, member = ISubscriptionManager( self._mlist).register(bart) # There should now be one email message in the virgin queue, i.e. the # confirmation message sent to Bart. items = get_queue_messages('virgin', expected_count=1) msg = items[0].msg # Confirmations come first, so this one goes to the subscriber. self.assertEqual(msg['to'], '*****@*****.**') confirm, token = str(msg['subject']).split() self.assertEqual(confirm, 'confirm') self.assertEqual(token, self._token) # Craft a confirmation response with the expected tokens. user_response = Message() user_response['From'] = '*****@*****.**' user_response['To'] = 'test-confirm+{}@example.com'.format(token) user_response['Subject'] = 'Re: confirm {}'.format(token) user_response.set_payload('') # Process the message through the command runner. config.switchboards['command'].enqueue(user_response, listid='test.example.com') make_testable_runner(CommandRunner, 'command').run() # There are now two messages in the virgin queue. One is going to the # subscriber containing the results of their confirmation message, and # the other is to the moderators informing them that they need to # handle the moderation queue. items = get_queue_messages('virgin', expected_count=2) if items[0].msg['to'] == '*****@*****.**': results = items[0].msg moderator_msg = items[1].msg else: results = items[1].msg moderator_msg = items[0].msg # Check the moderator message first. self.assertEqual(moderator_msg['to'], '*****@*****.**') self.assertEqual( moderator_msg['subject'], 'New subscription request to Test from [email protected]') lines = moderator_msg.get_payload().splitlines() self.assertEqual(lines[-2].strip(), 'For: Bart Person <*****@*****.**>') self.assertEqual(lines[-1].strip(), 'List: [email protected]') # Now check the results message. self.assertEqual(str(results['subject']), 'The results of your email commands') self.assertMultiLineEqual( results.get_payload(), """\ The results of your email command are provided below. - Original message details: From: [email protected] Subject: Re: confirm {} Date: n/a Message-ID: n/a - Results: Confirmed - Done. """.format(token))
def setUp(self): self._mlist = create_list('*****@*****.**') anne = subscribe(self._mlist, 'Anne', email='*****@*****.**') self._token, token_owner, member = ISubscriptionManager( self._mlist).unregister(anne.address)