diff options
Diffstat (limited to 'src/mailman/app')
| -rw-r--r-- | src/mailman/app/registrar.py | 106 | ||||
| -rw-r--r-- | src/mailman/app/subscriptions.py | 2 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_registrar.py | 170 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_registration.py | 128 | ||||
| -rw-r--r-- | src/mailman/app/workflow.py | 16 |
5 files changed, 198 insertions, 224 deletions
diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py index db68432d3..5c4c46c1f 100644 --- a/src/mailman/app/registrar.py +++ b/src/mailman/app/registrar.py @@ -25,18 +25,13 @@ __all__ = [ import logging +from mailman.app.subscriptions import SubscriptionWorkflow from mailman.core.i18n import _ from mailman.email.message import UserNotification -from mailman.interfaces.address import IEmailValidator -from mailman.interfaces.listmanager import IListManager -from mailman.interfaces.member import DeliveryMode, MemberRole from mailman.interfaces.pending import IPendable, IPendings from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar from mailman.interfaces.templates import ITemplateLoader -from mailman.interfaces.usermanager import IUserManager -from mailman.utilities.datetime import now from zope.component import getUtility -from zope.event import notify from zope.interface import implementer @@ -54,92 +49,27 @@ class PendableRegistration(dict): class Registrar: """Handle registrations and confirmations for subscriptions.""" - def register(self, mlist, email, display_name=None, delivery_mode=None): + def __init__(self, mlist): + self._mlist = mlist + + def register(self, subscriber=None, *, + pre_verified=False, pre_confirmed=False, pre_approved=False): """See `IRegistrar`.""" - if delivery_mode is None: - delivery_mode = DeliveryMode.regular - # First, do validation on the email address. If the address is - # invalid, it will raise an exception, otherwise it just returns. - getUtility(IEmailValidator).validate(email) - # Create a pendable for the registration. - pendable = PendableRegistration( - type=PendableRegistration.PEND_KEY, - email=email, - display_name=display_name, - delivery_mode=delivery_mode.name, - list_id=mlist.list_id) - token = getUtility(IPendings).add(pendable) - # We now have everything we need to begin the confirmation dance. - # Trigger the event to start the ball rolling, and return the - # generated token. - notify(ConfirmationNeededEvent(mlist, pendable, token)) - return token + workflow = SubscriptionWorkflow( + self._mlist, subscriber, + pre_verified=pre_verified, + pre_confirmed=pre_confirmed, + pre_approved=pre_approved) + list(workflow) + return workflow.token def confirm(self, token): """See `IRegistrar`.""" - # For convenience - pendable = getUtility(IPendings).confirm(token) - if pendable is None: - return False - missing = object() - email = pendable.get('email', missing) - display_name = pendable.get('display_name', missing) - pended_delivery_mode = pendable.get('delivery_mode', 'regular') - try: - delivery_mode = DeliveryMode[pended_delivery_mode] - except ValueError: - log.error('Invalid pended delivery_mode for {0}: {1}', - email, pended_delivery_mode) - delivery_mode = DeliveryMode.regular - if pendable.get('type') != PendableRegistration.PEND_KEY: - # It seems like it would be very difficult to accurately guess - # tokens, or brute force an attack on the SHA1 hash, so we'll just - # throw the pendable away in that case. It's possible we'll need - # to repend the event or adjust the API to handle this case - # better, but for now, the simpler the better. - return False - # We are going to end up with an IAddress for the verified address - # and an IUser linked to this IAddress. See if any of these objects - # currently exist in our database. - user_manager = getUtility(IUserManager) - address = (user_manager.get_address(email) - if email is not missing else None) - user = (user_manager.get_user(email) - if email is not missing else None) - # If there is neither an address nor a user matching the confirmed - # record, then create the user, which will in turn create the address - # and link the two together - if address is None: - assert user is None, 'How did we get a user but not an address?' - user = user_manager.create_user(email, display_name) - # Because the database changes haven't been flushed, we can't use - # IUserManager.get_address() to find the IAddress just created - # under the hood. Instead, iterate through the IUser's addresses, - # of which really there should be only one. - for address in user.addresses: - if address.email == email: - break - else: - raise AssertionError('Could not find expected IAddress') - elif user is None: - user = user_manager.create_user() - user.display_name = display_name - user.link(address) - else: - # The IAddress and linked IUser already exist, so all we need to - # do is verify the address. - pass - address.verified_on = now() - # If this registration is tied to a mailing list, subscribe the person - # to the list right now. That will generate a SubscriptionEvent, - # which can be used to send a welcome message. - list_id = pendable.get('list_id') - if list_id is not None: - mlist = getUtility(IListManager).get_by_list_id(list_id) - if mlist is not None: - member = mlist.subscribe(address, MemberRole.member) - member.preferences.delivery_mode = delivery_mode - return True + workflow = SubscriptionWorkflow(self._mlist) + workflow.token = token + workflow.debug = True + workflow.restore() + list(workflow) def discard(self, token): # Throw the record away. diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py index 999b04270..7b46aee84 100644 --- a/src/mailman/app/subscriptions.py +++ b/src/mailman/app/subscriptions.py @@ -297,7 +297,7 @@ class SubscriptionWorkflow(Workflow): if self.address.verified_on is None: self.address.verified_on = now() # The next step depends on the mailing list's subscription policy. - next_step = ('moderation_check' + next_step = ('moderation_checks' if self.mlist.subscription_policy in ( SubscriptionPolicy.moderate, SubscriptionPolicy.confirm_then_moderate, diff --git a/src/mailman/app/tests/test_registrar.py b/src/mailman/app/tests/test_registrar.py new file mode 100644 index 000000000..c8e0044de --- /dev/null +++ b/src/mailman/app/tests/test_registrar.py @@ -0,0 +1,170 @@ +# Copyright (C) 2012-2015 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Test email address registration.""" + +__all__ = [ + 'TestRegistrar', + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.interfaces.mailinglist import SubscriptionPolicy +from mailman.interfaces.pending import IPendings +from mailman.interfaces.registrar import IRegistrar +from mailman.interfaces.usermanager import IUserManager +from mailman.testing.layers import ConfigLayer +from mailman.utilities.datetime import now +from zope.component import getUtility + + + +class TestRegistrar(unittest.TestCase): + """Test registration.""" + + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('ant@example.com') + self._registrar = IRegistrar(self._mlist) + self._pendings = getUtility(IPendings) + self._anne = getUtility(IUserManager).create_address( + 'anne@example.com') + + def test_unique_token(self): + # Registering a subscription request provides a unique token associated + # with a pendable. + self.assertEqual(self._pendings.count(), 0) + token = self._registrar.register(self._anne) + self.assertIsNotNone(token) + 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['address'], 'anne@example.com') + + def test_no_token(self): + # Registering a subscription request where no confirmation or + # moderation steps are needed, leaves us with no token, since there's + # nothing more to do. + self._mlist.subscription_policy = SubscriptionPolicy.open + self._anne.verified_on = now() + token = self._registrar.register(self._anne) + self.assertIsNone(token) + record = self._pendings.confirm(token, expunge=False) + self.assertIsNone(record) + + def test_is_subscribed(self): + # Where no confirmation or moderation steps are needed, registration + # happens immediately. + self._mlist.subscription_policy = SubscriptionPolicy.open + self._anne.verified_on = now() + self._registrar.register(self._anne) + member = self._mlist.regular_members.get_member('anne@example.com') + self.assertEqual(member.address, self._anne) + + 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 = self._registrar.register(self._anne) + self.assertIsNotNone(token) + member = self._mlist.regular_members.get_member('anne@example.com') + self.assertIsNone(member) + # Now confirm the subscription. + self._registrar.confirm(token) + member = self._mlist.regular_members.get_member('anne@example.com') + 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 = self._registrar.register(self._anne) + self.assertIsNotNone(token) + member = self._mlist.regular_members.get_member('anne@example.com') + self.assertIsNone(member) + # Now confirm the subscription. + self._registrar.confirm(token) + member = self._mlist.regular_members.get_member('anne@example.com') + 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 = self._registrar.register(self._anne) + self.assertIsNotNone(token) + member = self._mlist.regular_members.get_member('anne@example.com') + self.assertIsNone(member) + # Now confirm the subscription. + self._registrar.confirm(token) + member = self._mlist.regular_members.get_member('anne@example.com') + 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 = self._registrar.register(self._anne) + self.assertIsNotNone(token) + member = self._mlist.regular_members.get_member('anne@example.com') + self.assertIsNone(member) + # Now confirm the subscription, and wait for the moderator to approve + # the subscription. She is still not subscribed. + self._registrar.confirm(token) + member = self._mlist.regular_members.get_member('anne@example.com') + self.assertIsNone(member) + # Confirm once more, this time as the moderator approving the + # subscription. Now she's a member. + self._registrar.confirm(token) + member = self._mlist.regular_members.get_member('anne@example.com') + 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 = self._registrar.register(self._anne) + self.assertIsNotNone(token) + member = self._mlist.regular_members.get_member('anne@example.com') + 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) diff --git a/src/mailman/app/tests/test_registration.py b/src/mailman/app/tests/test_registration.py deleted file mode 100644 index ccc485492..000000000 --- a/src/mailman/app/tests/test_registration.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (C) 2012-2015 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Test email address registration.""" - -__all__ = [ - 'TestEmailValidation', - 'TestRegistration', - ] - - -import unittest - -from mailman.app.lifecycle import create_list -from mailman.interfaces.address import InvalidEmailAddressError -from mailman.interfaces.pending import IPendings -from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar -from mailman.testing.helpers import event_subscribers -from mailman.testing.layers import ConfigLayer -from zope.component import getUtility - - - -class TestEmailValidation(unittest.TestCase): - """Test basic email validation.""" - - layer = ConfigLayer - - def setUp(self): - self.registrar = getUtility(IRegistrar) - self.mlist = create_list('alpha@example.com') - - def test_empty_string_is_invalid(self): - self.assertRaises(InvalidEmailAddressError, - self.registrar.register, self.mlist, - '') - - def test_no_spaces_allowed(self): - self.assertRaises(InvalidEmailAddressError, - self.registrar.register, self.mlist, - 'some name@example.com') - - def test_no_angle_brackets(self): - self.assertRaises(InvalidEmailAddressError, - self.registrar.register, self.mlist, - '<script>@example.com') - - def test_ascii_only(self): - self.assertRaises(InvalidEmailAddressError, - self.registrar.register, self.mlist, - '\xa0@example.com') - - def test_domain_required(self): - self.assertRaises(InvalidEmailAddressError, - self.registrar.register, self.mlist, - 'noatsign') - - def test_full_domain_required(self): - self.assertRaises(InvalidEmailAddressError, - self.registrar.register, self.mlist, - 'nodom@ain') - - - -class TestRegistration(unittest.TestCase): - """Test registration.""" - - layer = ConfigLayer - - def setUp(self): - self.registrar = getUtility(IRegistrar) - self.mlist = create_list('alpha@example.com') - - def test_confirmation_event_received(self): - # Registering an email address generates an event. - def capture_event(event): - self.assertIsInstance(event, ConfirmationNeededEvent) - with event_subscribers(capture_event): - self.registrar.register(self.mlist, 'anne@example.com') - - def test_event_mlist(self): - # The event has a reference to the mailing list being subscribed to. - def capture_event(event): - self.assertIs(event.mlist, self.mlist) - with event_subscribers(capture_event): - self.registrar.register(self.mlist, 'anne@example.com') - - def test_event_pendable(self): - # The event has an IPendable which contains additional information. - def capture_event(event): - pendable = event.pendable - self.assertEqual(pendable['type'], 'registration') - self.assertEqual(pendable['email'], 'anne@example.com') - # The key is present, but the value is None. - self.assertIsNone(pendable['display_name']) - # The default is regular delivery. - self.assertEqual(pendable['delivery_mode'], 'regular') - self.assertEqual(pendable['list_id'], 'alpha.example.com') - with event_subscribers(capture_event): - self.registrar.register(self.mlist, 'anne@example.com') - - def test_token(self): - # Registering the email address returns a token, and this token links - # back to the pendable. - captured_events = [] - def capture_event(event): - captured_events.append(event) - with event_subscribers(capture_event): - token = self.registrar.register(self.mlist, 'anne@example.com') - self.assertEqual(len(captured_events), 1) - event = captured_events[0] - self.assertEqual(event.token, token) - pending = getUtility(IPendings).confirm(token) - self.assertEqual(pending, event.pendable) diff --git a/src/mailman/app/workflow.py b/src/mailman/app/workflow.py index 8006f8e51..b83d1c3aa 100644 --- a/src/mailman/app/workflow.py +++ b/src/mailman/app/workflow.py @@ -145,10 +145,12 @@ class Workflow: def restore(self): state_manager = getUtility(IWorkflowStateManager) state = state_manager.restore(self.__class__.__name__, self.token) - if state is not None: - self._next.clear() - if state.step: - self._next.append(state.step) - if state.data is not None: - for attr, value in json.loads(state.data).items(): - setattr(self, attr, value) + if state is None: + # The token doesn't exist in the database. + raise LookupError(self.token) + self._next.clear() + if state.step: + self._next.append(state.step) + if state.data is not None: + for attr, value in json.loads(state.data).items(): + setattr(self, attr, value) |
