diff options
| author | Barry Warsaw | 2015-04-14 12:46:11 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2015-04-14 12:46:11 -0400 |
| commit | 2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd (patch) | |
| tree | 4ba9e86dd16b53c623410e66c459dc394008b698 /src | |
| parent | 24c01dbd8e93acdc61884b3b9783a0e71fd6df23 (diff) | |
| download | mailman-2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd.tar.gz mailman-2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd.tar.zst mailman-2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd.zip | |
Diffstat (limited to 'src')
| -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 | ||||
| -rw-r--r-- | src/mailman/commands/eml_confirm.py | 2 | ||||
| -rw-r--r-- | src/mailman/commands/eml_membership.py | 3 | ||||
| -rw-r--r-- | src/mailman/commands/tests/test_confirm.py | 6 | ||||
| -rw-r--r-- | src/mailman/config/configure.zcml | 11 | ||||
| -rw-r--r-- | src/mailman/interfaces/pending.py | 4 | ||||
| -rw-r--r-- | src/mailman/interfaces/registrar.py | 62 | ||||
| -rw-r--r-- | src/mailman/model/docs/registration.rst | 353 | ||||
| -rw-r--r-- | src/mailman/model/pending.py | 6 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_registrar.py | 64 | ||||
| -rw-r--r-- | src/mailman/runners/docs/command.rst | 4 | ||||
| -rw-r--r-- | src/mailman/runners/tests/test_confirm.py | 4 | ||||
| -rw-r--r-- | src/mailman/runners/tests/test_join.py | 2 |
17 files changed, 307 insertions, 636 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) diff --git a/src/mailman/commands/eml_confirm.py b/src/mailman/commands/eml_confirm.py index a28a3f728..4c6039aad 100644 --- a/src/mailman/commands/eml_confirm.py +++ b/src/mailman/commands/eml_confirm.py @@ -53,7 +53,7 @@ class Confirm: return ContinueProcessing.yes tokens.add(token) results.confirms = tokens - succeeded = getUtility(IRegistrar).confirm(token) + succeeded = IRegistrar(mlist).confirm(token) if succeeded: print(_('Confirmed'), file=results) return ContinueProcessing.yes diff --git a/src/mailman/commands/eml_membership.py b/src/mailman/commands/eml_membership.py index 059b9b634..b42116b74 100644 --- a/src/mailman/commands/eml_membership.py +++ b/src/mailman/commands/eml_membership.py @@ -86,8 +86,7 @@ used. if len(members) > 0: print(_('$person is already a member'), file=results) else: - getUtility(IRegistrar).register(mlist, address, - display_name, delivery_mode) + IRegistrar(mlist).register(address) print(_('Confirmation email sent to $person'), file=results) return ContinueProcessing.yes diff --git a/src/mailman/commands/tests/test_confirm.py b/src/mailman/commands/tests/test_confirm.py index dd168454f..2f6a8088f 100644 --- a/src/mailman/commands/tests/test_confirm.py +++ b/src/mailman/commands/tests/test_confirm.py @@ -29,6 +29,7 @@ from mailman.commands.eml_confirm import Confirm from mailman.email.message import Message from mailman.interfaces.command import ContinueProcessing from mailman.interfaces.registrar import IRegistrar +from mailman.interfaces.usermanager import IUserManager from mailman.runners.command import Results from mailman.testing.helpers import get_queue_messages, reset_the_world from mailman.testing.layers import ConfigLayer @@ -43,8 +44,9 @@ class TestConfirm(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') - self._token = getUtility(IRegistrar).register( - self._mlist, 'anne@example.com', 'Anne Person') + anne = getUtility(IUserManager).create_address( + 'anne@example.com', 'Anne Person') + self._token = IRegistrar(self._mlist).register(anne) self._command = Confirm() # Clear the virgin queue. get_queue_messages('virgin') diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml index 1f2283b02..632771d42 100644 --- a/src/mailman/config/configure.zcml +++ b/src/mailman/config/configure.zcml @@ -40,6 +40,12 @@ factory="mailman.model.requests.ListRequests" /> + <adapter + for="mailman.interfaces.mailinglist.IMailingList" + provides="mailman.interfaces.registrar.IRegistrar" + factory="mailman.app.registrar.Registrar" + /> + <utility provides="mailman.interfaces.bounce.IBounceProcessor" factory="mailman.model.bounce.BounceProcessor" @@ -88,11 +94,6 @@ /> <utility - provides="mailman.interfaces.registrar.IRegistrar" - factory="mailman.app.registrar.Registrar" - /> - - <utility provides="mailman.interfaces.styles.IStyleManager" factory="mailman.styles.manager.StyleManager" /> diff --git a/src/mailman/interfaces/pending.py b/src/mailman/interfaces/pending.py index 09c8b44cb..7420fcdb2 100644 --- a/src/mailman/interfaces/pending.py +++ b/src/mailman/interfaces/pending.py @@ -82,11 +82,11 @@ class IPendings(Interface): :return: A token string for inclusion in urls and email confirmations. """ - def confirm(token, expunge=True): + def confirm(token, *, expunge=True): """Return the IPendable matching the token. :param token: The token string for the IPendable given by the `.add()` - method. + method, or None if there is no record associated with the token. :param expunge: A flag indicating whether the pendable record should also be removed from the database or not. :return: The matching IPendable or None if no match was found. diff --git a/src/mailman/interfaces/registrar.py b/src/mailman/interfaces/registrar.py index 504442f7e..d67334775 100644 --- a/src/mailman/interfaces/registrar.py +++ b/src/mailman/interfaces/registrar.py @@ -47,58 +47,58 @@ class ConfirmationNeededEvent: class IRegistrar(Interface): - """Interface for registering and verifying email addresses and users. + """Interface for subscribing addresses and users. This is a higher level interface to user registration, email address confirmation, etc. than the IUserManager. The latter does no validation, syntax checking, or confirmation, while this interface does. """ - def register(mlist, email, display_name=None, delivery_mode=None): - """Register the email address, requesting verification. + def register(mlist, subscriber=None, *, + pre_verified=False, pre_confirmed=False, pre_approved=False): + """Subscribe an address or user according to subscription policies. - No `IAddress` or `IUser` is created during this step, but after - successful confirmation, it is guaranteed that an `IAddress` with a - linked `IUser` will exist. When a verified `IAddress` matching - `email` already exists, this method will do nothing, except link a new - `IUser` to the `IAddress` if one is not yet associated with the - email address. + The mailing list's subscription policy is used to subscribe + `subscriber` to the given mailing list. The subscriber can be + an ``IUser``, in which case the user must have a preferred + address, and that preferred address will be subscribed. The + subscriber can also be an ``IAddress``, in which case the + address will be subscribed. - In all cases, the email address is sanity checked for validity first. + The workflow may pause (i.e. be serialized, saved, and + suspended) when some out-of-band confirmation step is required. + For example, if the user must confirm, or the moderator must + approve the subscription. Use the ``confirm(token)`` method to + resume the workflow. - :param mlist: The mailing list that is the focus of this registration. + :param mlist: The mailing list to subscribe to. :type mlist: `IMailingList` - :param email: The email address to register. - :type email: str - :param display_name: The optional display name of the user. - :type display_name: str - :param delivery_mode: The optional delivery mode for this - registration. If not given, regular delivery is used. - :type delivery_mode: `DeliveryMode` + :param subscriber: The user or address to subscribe. + :type email: ``IUser`` or ``IAddress`` :return: The confirmation token string. :rtype: str - :raises InvalidEmailAddressError: if the address is not allowed. + :raises MembershipIsBannedError: when the address being subscribed + appears in the global or list-centric bans. """ def confirm(token): - """Confirm the pending registration matched to the given `token`. + """Continue any paused workflow. - Confirmation ensures that the IAddress exists and is linked to an - IUser, with the latter being created and linked if necessary. + Confirmation may occur after the user confirms their + subscription request, or their email address must be verified, + or the moderator must approve the subscription request. - :param token: A token matching a pending event with a type of - 'registration'. - :return: Boolean indicating whether the confirmation succeeded or - not. It may fail if the token is no longer in the database, or if - the token did not match a registration event. + :param token: A token matching a workflow. + :raises LookupError: when no workflow is associated with the token. """ def discard(token): - """Discard the pending registration matched to the given `token`. - - The event record is discarded and the IAddress is not verified. No - IUser is created. + """Discard the workflow matched to the given `token`. :param token: A token matching a pending event with a type of 'registration'. + :raises LookupError: when no workflow is associated with the token. """ + + def evict(): + """Evict all saved workflows which have expired.""" diff --git a/src/mailman/model/docs/registration.rst b/src/mailman/model/docs/registration.rst index 2d2aa8ec7..fc7ad6f1a 100644 --- a/src/mailman/model/docs/registration.rst +++ b/src/mailman/model/docs/registration.rst @@ -1,333 +1,90 @@ -==================== -Address registration -==================== - -Before users can join a mailing list, they must first register with Mailman. -The only thing they must supply is an email address, although there is -additional information they may supply. All registered email addresses must -be verified before Mailman will send them any list traffic. +============ +Registration +============ -The ``IUserManager`` manages users, but it does so at a fairly low level. -Specifically, it does not handle verification, email address syntax validity -checks, etc. The ``IRegistrar`` is the interface to the object handling all -this stuff. +When a user wants to join a mailing list, they must register and verify their +email address. Then depending on how the mailing list is configured, they may +need to confirm their subscription and have it approved by the list +moderator. The ``IRegistrar`` interface manages this work flow. >>> from mailman.interfaces.registrar import IRegistrar - >>> from zope.component import getUtility - >>> registrar = getUtility(IRegistrar) - -Here is a helper function to check the token strings. - - >>> def check_token(token): - ... assert isinstance(token, str), 'Not a string' - ... assert len(token) == 40, 'Unexpected length: %d' % len(token) - ... assert token.isalnum(), 'Not alphanumeric' - ... print('ok') - -Here is a helper function to extract tokens from confirmation messages. - - >>> import re - >>> cre = re.compile('http://lists.example.com/confirm/(.*)') - >>> def extract_token(msg): - ... mo = cre.search(msg.get_payload()) - ... return mo.group(1) +Registrars adapt mailing lists. -Invalid email addresses -======================= - -Addresses are registered within the context of a mailing list, mostly so that -confirmation emails can come from some place. You also need the email -address of the user who is registering. - - >>> mlist = create_list('alpha@example.com') + >>> from mailman.interfaces.mailinglist import SubscriptionPolicy + >>> mlist = create_list('ant@example.com') >>> mlist.send_welcome_message = False + >>> mlist.subscription_policy = SubscriptionPolicy.open + >>> registrar = IRegistrar(mlist) -Some amount of sanity checks are performed on the email address, although -honestly, not as much as probably should be done. Still, some patently bad -addresses are rejected outright. - - -Register an email address -========================= - -Registration of an unknown address creates nothing until the confirmation step -is complete. No ``IUser`` or ``IAddress`` is created at registration time, -but a record is added to the pending database, and the token for that record -is returned. - - >>> token = registrar.register(mlist, 'aperson@example.com', 'Anne Person') - >>> check_token(token) - ok - -There should be no records in the user manager for this address yet. +Usually, addresses are registered, but users with preferred addresses can be +registered too. >>> from mailman.interfaces.usermanager import IUserManager >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - >>> print(user_manager.get_user('aperson@example.com')) - None - >>> print(user_manager.get_address('aperson@example.com')) - None - -But this address is waiting for confirmation. - - >>> from mailman.interfaces.pending import IPendings - >>> pendingdb = getUtility(IPendings) - - >>> dump_msgdata(pendingdb.confirm(token, expunge=False)) - delivery_mode: regular - display_name : Anne Person - email : aperson@example.com - list_id : alpha.example.com - type : registration - - -Verification by email -===================== - -There is also a verification email sitting in the virgin queue now. This -message is sent to the user in order to verify the registered address. - - >>> from mailman.testing.helpers import get_queue_messages - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> print(items[0].msg.as_string()) - MIME-Version: 1.0 - ... - Subject: confirm ... - From: alpha-confirm+...@example.com - To: aperson@example.com - ... - <BLANKLINE> - Email Address Registration Confirmation - <BLANKLINE> - Hello, this is the GNU Mailman server at example.com. - <BLANKLINE> - We have received a registration request for the email address - <BLANKLINE> - aperson@example.com - <BLANKLINE> - 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. Or you can visit this - web page - <BLANKLINE> - http://lists.example.com/confirm/... - <BLANKLINE> - 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 - <BLANKLINE> - alpha-owner@example.com - <BLANKLINE> - >>> dump_msgdata(items[0].msgdata) - _parsemsg : False - listid : alpha.example.com - nodecorate : True - recipients : {'aperson@example.com'} - reduced_list_headers: True - version : 3 - -The confirmation token shows up in several places, each of which provides an -easy way for the user to complete the confirmation. The token will always -appear in a URL in the body of the message. - - >>> sent_token = extract_token(items[0].msg) - >>> sent_token == token - True - -The same token will appear in the ``From`` header. - - >>> items[0].msg['from'] == 'alpha-confirm+' + token + '@example.com' - True - -It will also appear in the ``Subject`` header. + >>> anne = getUtility(IUserManager).create_address( + ... 'anne@example.com', 'Anne Person') - >>> items[0].msg['subject'] == 'confirm ' + token - True - -The user would then validate their registered address by clicking on a url or -responding to the message. Either way, the confirmation process extracts the -token and uses that to confirm the pending registration. - - >>> registrar.confirm(token) - True - -Now, there is an `IAddress` in the database matching the address, as well as -an `IUser` linked to this address. The `IAddress` is verified. - - >>> found_address = user_manager.get_address('aperson@example.com') - >>> found_address - <Address: Anne Person <aperson@example.com> [verified] at ...> - >>> found_user = user_manager.get_user('aperson@example.com') - >>> found_user - <User "Anne Person" (...) at ...> - >>> found_user.controls(found_address.email) - True - >>> from datetime import datetime - >>> isinstance(found_address.verified_on, datetime) - True +Register an email address +========================= -Non-standard registrations -========================== +When the registration steps involve confirmation or moderator approval, the +process will pause until these steps are completed. A unique token is created +which represents this work flow. -If you try to confirm a registration token twice, of course only the first one -will work. The second one is ignored. +Anne attempts to join the mailing list. - >>> token = registrar.register(mlist, 'bperson@example.com') - >>> check_token(token) - ok - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> sent_token = extract_token(items[0].msg) - >>> token == sent_token - True - >>> registrar.confirm(token) - True - >>> registrar.confirm(token) - False + >>> token = registrar.register(anne) -If an address is in the system, but that address is not linked to a user yet -and the address is not yet validated, then no user is created until the -confirmation step is completed. +Because her email address has not yet been verified, she has not yet become a +member of the mailing list. - >>> user_manager.create_address('cperson@example.com') - <Address: cperson@example.com [not verified] at ...> - >>> token = registrar.register( - ... mlist, 'cperson@example.com', 'Claire Person') - >>> print(user_manager.get_user('cperson@example.com')) + >>> print(mlist.members.get_member('anne@example.com')) None - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> sent_token = extract_token(items[0].msg) - >>> registrar.confirm(sent_token) - True - >>> user_manager.get_user('cperson@example.com') - <User "Claire Person" (...) at ...> - >>> user_manager.get_address('cperson@example.com') - <Address: cperson@example.com [verified] at ...> - -Even if the address being registered has already been verified, the -registration sends a confirmation. - - >>> token = registrar.register(mlist, 'cperson@example.com') - >>> token is not None - True - -Discarding -========== +Once she verifies her email address, she will become a member of the mailing +list. In this case, verifying implies that she also confirms her wish to join +the mailing list. -A confirmation token can also be discarded, say if the user changes his or her -mind about registering. When discarded, no `IAddress` or `IUser` is created. -:: + >>> registrar.confirm(token) + >>> mlist.members.get_member('anne@example.com') + <Member: Anne Person <anne@example.com> on ant@example.com + as MemberRole.member> - >>> token = registrar.register(mlist, 'eperson@example.com', 'Elly Person') - >>> check_token(token) - ok - >>> registrar.discard(token) - >>> print(pendingdb.confirm(token)) - None - >>> print(user_manager.get_address('eperson@example.com')) - None - >>> print(user_manager.get_user('eperson@example.com')) - None - # Clear the virgin queue of all the preceding confirmation messages. - >>> ignore = get_queue_messages('virgin') +Register a user +=============== +Users can also register, but they must have a preferred address. The mailing +list will deliver messages to this preferred address. -Registering a new address for an existing user -============================================== + >>> bart = getUtility(IUserManager).make_user( + ... 'bart@example.com', 'Bart Person') -When a new address for an existing user is registered, there isn't too much -different except that the new address will still need to be verified before it -can be used. -:: +Bart verifies his address and makes it his preferred address. >>> from mailman.utilities.datetime import now - >>> dperson = user_manager.create_user( - ... 'dperson@example.com', 'Dave Person') - >>> dperson - <User "Dave Person" (...) at ...> - >>> address = user_manager.get_address('dperson@example.com') - >>> address.verified_on = now() - - >>> from operator import attrgetter - >>> dump_list(repr(address) for address in dperson.addresses) - <Address: Dave Person <dperson@example.com> [verified] at ...> - >>> dperson.register('david.person@example.com', 'David Person') - <Address: David Person <david.person@example.com> [not verified] at ...> - >>> token = registrar.register(mlist, 'david.person@example.com') - - >>> items = get_queue_messages('virgin') - >>> len(items) - 1 - >>> sent_token = extract_token(items[0].msg) - >>> registrar.confirm(sent_token) - True - >>> user = user_manager.get_user('david.person@example.com') - >>> user is dperson - True - >>> user - <User "Dave Person" (...) at ...> - >>> dump_list(repr(address) for address in user.addresses) - <Address: Dave Person <dperson@example.com> [verified] at ...> - <Address: David Person <david.person@example.com> [verified] at ...> - - -Corner cases -============ - -If you try to confirm a token that doesn't exist in the pending database, the -confirm method will just return False. - - >>> registrar.confirm(bytes(b'no token')) - False - -Likewise, if you try to confirm, through the `IRegistrar` interface, a token -that doesn't match a registration event, you will get ``None``. However, the -pending event matched with that token will still be removed. -:: - - >>> from mailman.interfaces.pending import IPendable - >>> from zope.interface import implementer - - >>> @implementer(IPendable) - ... class SimplePendable(dict): - ... pass - - >>> pendable = SimplePendable(type='foo', bar='baz') - >>> token = pendingdb.add(pendable) - >>> registrar.confirm(token) - False - >>> print(pendingdb.confirm(token)) - None - - -Registration and subscription -============================= + >>> preferred = list(bart.addresses)[0] + >>> preferred.verified_on = now() + >>> bart.preferred_address = preferred -Fred registers with Mailman at the same time that he subscribes to a mailing -list. +The mailing list's subscription policy does not require Bart to confirm his +subscription, but the moderate does want to approve all subscriptions. - >>> token = registrar.register( - ... mlist, 'fred.person@example.com', 'Fred Person') + >>> mlist.subscription_policy = SubscriptionPolicy.moderate -Before confirmation, Fred is not a member of the mailing list. +Now when Bart registers as a user for the mailing list, a token will still be +generated, but this is only used by the moderator. At first, Bart is not +subscribed to the mailing list. - >>> print(mlist.members.get_member('fred.person@example.com')) + >>> token = registrar.register(bart) + >>> print(mlist.members.get_member('bart@example.com')) None -But after confirmation, he is. +When the moderator confirms Bart's subscription, he joins the mailing list. >>> registrar.confirm(token) - True - >>> print(mlist.members.get_member('fred.person@example.com')) - <Member: Fred Person <fred.person@example.com> - on alpha@example.com as MemberRole.member> + >>> mlist.members.get_member('bart@example.com') + <Member: Bart Person <bart@example.com> on ant@example.com + as MemberRole.member> diff --git a/src/mailman/model/pending.py b/src/mailman/model/pending.py index 77b68bd2d..8eb2ab8ba 100644 --- a/src/mailman/model/pending.py +++ b/src/mailman/model/pending.py @@ -128,7 +128,7 @@ class Pendings: return token @dbconnection - def confirm(self, store, token, expunge=True): + def confirm(self, store, token, *, expunge=True): # Token can come in as a unicode, but it's stored in the database as # bytes. They must be ascii. pendings = store.query(Pended).filter_by(token=str(token)) @@ -165,3 +165,7 @@ class Pendings: for keyvalue in q: store.delete(keyvalue) store.delete(pending) + + @dbconnection + def count(self, store): + return store.query(Pended).count() diff --git a/src/mailman/model/tests/test_registrar.py b/src/mailman/model/tests/test_registrar.py deleted file mode 100644 index e6df7f0d1..000000000 --- a/src/mailman/model/tests/test_registrar.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (C) 2014-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 `IRegistrar`.""" - -__all__ = [ - 'TestRegistrar', - ] - - -import unittest - -from functools import partial -from mailman.app.lifecycle import create_list -from mailman.interfaces.address import InvalidEmailAddressError -from mailman.interfaces.registrar import IRegistrar -from mailman.testing.layers import ConfigLayer -from zope.component import getUtility - - - -class TestRegistrar(unittest.TestCase): - layer = ConfigLayer - - def setUp(self): - mlist = create_list('test@example.com') - self._register = partial(getUtility(IRegistrar).register, mlist) - - def test_invalid_empty_string(self): - self.assertRaises(InvalidEmailAddressError, self._register, '') - - def test_invalid_space_in_name(self): - self.assertRaises(InvalidEmailAddressError, self._register, - 'some name@example.com') - - def test_invalid_funky_characters(self): - self.assertRaises(InvalidEmailAddressError, self._register, - '<script>@example.com') - - def test_invalid_nonascii(self): - self.assertRaises(InvalidEmailAddressError, self._register, - '\xa0@example.com') - - def test_invalid_no_at_sign(self): - self.assertRaises(InvalidEmailAddressError, self._register, - 'noatsign') - - def test_invalid_no_domain(self): - self.assertRaises(InvalidEmailAddressError, self._register, - 'nodom@ain') diff --git a/src/mailman/runners/docs/command.rst b/src/mailman/runners/docs/command.rst index 82ee33fbc..5cfbf7cfb 100644 --- a/src/mailman/runners/docs/command.rst +++ b/src/mailman/runners/docs/command.rst @@ -140,9 +140,7 @@ address, and the other is the results of his email command. >>> len(messages) 2 - >>> from mailman.interfaces.registrar import IRegistrar - >>> from zope.component import getUtility - >>> registrar = getUtility(IRegistrar) + >>> registrar = IRegistrar(mlist) >>> for item in messages: ... subject = item.msg['subject'] ... print('Subject:', subject) diff --git a/src/mailman/runners/tests/test_confirm.py b/src/mailman/runners/tests/test_confirm.py index 090451ce7..71ec5988d 100644 --- a/src/mailman/runners/tests/test_confirm.py +++ b/src/mailman/runners/tests/test_confirm.py @@ -46,14 +46,14 @@ class TestConfirm(unittest.TestCase): layer = ConfigLayer def setUp(self): - registrar = getUtility(IRegistrar) self._commandq = config.switchboards['command'] self._runner = make_testable_runner(CommandRunner, 'command') with transaction(): # Register a subscription requiring confirmation. self._mlist = create_list('test@example.com') self._mlist.send_welcome_message = False - self._token = registrar.register(self._mlist, 'anne@example.org') + anne = getUtility(IUserManager).create_address('anne@example.org') + self._token = IRegistrar(self._mlist).register(anne) def test_confirm_with_re_prefix(self): subject = 'Re: confirm {0}'.format(self._token) diff --git a/src/mailman/runners/tests/test_join.py b/src/mailman/runners/tests/test_join.py index 4006675e4..f968433ba 100644 --- a/src/mailman/runners/tests/test_join.py +++ b/src/mailman/runners/tests/test_join.py @@ -160,7 +160,7 @@ class TestJoinWithDigests(unittest.TestCase): subject_words = str(messages[1].msg['subject']).split() self.assertEqual(subject_words[0], 'confirm') token = subject_words[1] - status = getUtility(IRegistrar).confirm(token) + status = IRegistrar(self._mlist).confirm(token) self.assertTrue(status, 'Confirmation failed') # Now, make sure that Anne is a member of the list and is receiving # digest deliveries. |
