summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2015-04-14 12:46:11 -0400
committerBarry Warsaw2015-04-14 12:46:11 -0400
commit2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd (patch)
tree4ba9e86dd16b53c623410e66c459dc394008b698 /src
parent24c01dbd8e93acdc61884b3b9783a0e71fd6df23 (diff)
downloadmailman-2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd.tar.gz
mailman-2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd.tar.zst
mailman-2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd.zip
Diffstat (limited to 'src')
-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
-rw-r--r--src/mailman/commands/eml_confirm.py2
-rw-r--r--src/mailman/commands/eml_membership.py3
-rw-r--r--src/mailman/commands/tests/test_confirm.py6
-rw-r--r--src/mailman/config/configure.zcml11
-rw-r--r--src/mailman/interfaces/pending.py4
-rw-r--r--src/mailman/interfaces/registrar.py62
-rw-r--r--src/mailman/model/docs/registration.rst353
-rw-r--r--src/mailman/model/pending.py6
-rw-r--r--src/mailman/model/tests/test_registrar.py64
-rw-r--r--src/mailman/runners/docs/command.rst4
-rw-r--r--src/mailman/runners/tests/test_confirm.py4
-rw-r--r--src/mailman/runners/tests/test_join.py2
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.