Example #1
0
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)
Example #2
0
 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)
Example #3
0
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)
Example #4
0
    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)
Example #6
0
 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')
Example #7
0
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')
Example #8
0
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)
Example #9
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')