summaryrefslogtreecommitdiff
path: root/src/mailman/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/app')
-rw-r--r--src/mailman/app/registrar.py106
-rw-r--r--src/mailman/app/subscriptions.py2
-rw-r--r--src/mailman/app/tests/test_registrar.py170
-rw-r--r--src/mailman/app/tests/test_registration.py128
-rw-r--r--src/mailman/app/workflow.py16
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)