summaryrefslogtreecommitdiff
path: root/src/mailman/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/app')
-rw-r--r--src/mailman/app/subscriptions.py177
-rw-r--r--src/mailman/app/tests/test_subscriptions.py124
2 files changed, 297 insertions, 4 deletions
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index cc363b30d..1e45da43e 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -19,10 +19,13 @@
__all__ = [
'SubscriptionService',
+ 'SubscriptionWorkflow',
'handle_ListDeletingEvent',
]
+import json
+from collections import deque
from operator import attrgetter
from sqlalchemy import and_, or_
from uuid import UUID
@@ -30,18 +33,23 @@ from zope.component import getUtility
from zope.interface import implementer
from mailman.app.membership import add_member, delete_member
+from mailman.app.moderator import hold_subscription
from mailman.core.constants import system_preferences
from mailman.database.transaction import dbconnection
+from mailman.interfaces.address import IAddress
from mailman.interfaces.listmanager import (
IListManager, ListDeletingEvent, NoSuchListError)
+from mailman.interfaces.mailinglist import SubscriptionPolicy
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.subscriptions import (
ISubscriptionService, MissingUserError, RequestRecord)
+from mailman.interfaces.user import IUser
from mailman.interfaces.usermanager import IUserManager
+from mailman.interfaces.workflowstate import IWorkflowStateManager
from mailman.model.member import Member
+from mailman.utilities.datetime import now
-
def _membership_sort_key(member):
"""Sort function for find_members().
@@ -51,7 +59,171 @@ def _membership_sort_key(member):
return (member.list_id, member.address.email, member.role.value)
-
+class Workflow:
+ """Generic workflow."""
+ # TODO: move this class to a more generic module
+
+ _save_key = ""
+ _save_attributes = []
+ _initial_state = []
+
+ def __init__(self):
+ self._next = deque(self._initial_state)
+
+ def __iter__(self):
+ return self
+
+ def _pop(self):
+ name = self._next.popleft()
+ step = getattr(self, '_step_{}'.format(name))
+ return step, name
+
+ def __next__(self):
+ try:
+ step, name = self._pop()
+ step()
+ except IndexError:
+ raise StopIteration
+ except:
+ raise
+
+ def save_state(self):
+ state_manager = getUtility(IWorkflowStateManager)
+ data = {attr: getattr(self, attr) for attr in self._save_attributes}
+ # Note: only the next step is saved, not the whole stack. Not an issue
+ # since there's never more than a single step in the queue anyway.
+ # If we want to support more than a single step in the queue AND want
+ # to support state saving/restoring, change this method and the
+ # restore_state() method.
+ if len(self._next) == 0:
+ step = None
+ elif len(self._next) == 1:
+ step = self._next[0]
+ else:
+ raise AssertionError(
+ "Can't save a workflow state with more than one step "
+ "in the queue")
+ state_manager.save(
+ self.__class__.__name__,
+ self._save_key,
+ step,
+ json.dumps(data))
+
+ def restore_state(self):
+ state_manager = getUtility(IWorkflowStateManager)
+ state = state_manager.restore(self.__class__.__name__, self._save_key)
+ 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)
+
+
+class SubscriptionWorkflow(Workflow):
+ """Workflow of a subscription request."""
+
+ _save_attributes = ["pre_verified", "pre_confirmed", "pre_approved"]
+ _initial_state = ["verification_check"]
+
+ def __init__(self, mlist, subscriber,
+ pre_verified, pre_confirmed, pre_approved):
+ super(SubscriptionWorkflow, self).__init__()
+ self.mlist = mlist
+ # The subscriber must be either an IUser or IAddress.
+ if IAddress.providedBy(subscriber):
+ self.address = subscriber
+ self.user = self.address.user
+ elif IUser.providedBy(subscriber):
+ self.address = subscriber.preferred_address
+ self.user = subscriber
+ self.subscriber = subscriber
+ self.pre_verified = pre_verified
+ self.pre_confirmed = pre_confirmed
+ self.pre_approved = pre_approved
+ # State saving
+ self._save_key = "{}:{}".format(self.mlist.list_id, self.address.email)
+
+ def _maybe_set_preferred_address(self):
+ if self.user is None:
+ # The address has no linked user so create one, link it, and set
+ # the user's preferred address.
+ assert self.address is not None, 'No address or user'
+ self.user = getUtility(IUserManager).make_user(self.address.email)
+ self.user.preferred_address = self.address
+ elif self.user.preferred_address is None:
+ assert self.address is not None, 'No address or user'
+ # The address has a linked user, but no preferred address is set
+ # yet. This is required, so use the address.
+ self.user.preferred_address = self.address
+
+ def _step_verification_check(self):
+ if self.address.verified_on is not None:
+ # The address is already verified. Give the user a preferred
+ # address if it doesn't already have one. We may still have to do
+ # a subscription confirmation check. See below.
+ self._maybe_set_preferred_address()
+ else:
+ # The address is not yet verified. Maybe we're pre-verifying it.
+ # If so, we also want to give the user a preferred address if it
+ # doesn't already have one. We may still have to do a
+ # subscription confirmation check. See below.
+ if self.pre_verified:
+ self.address.verified_on = now()
+ self._maybe_set_preferred_address()
+ else:
+ # Since the address was not already verified, and not
+ # pre-verified, we have to send a confirmation check, which
+ # doubles as a verification step. Skip to that now.
+ self._next.append("send_confirmation")
+ return
+ self._next.append("confirmation_check")
+
+ def _step_confirmation_check(self):
+ # Must the user confirm their subscription request? If the policy is
+ # open subscriptions, then we need neither confirmation nor moderator
+ # approval, so just subscribe them now.
+ if self.mlist.subscription_policy == SubscriptionPolicy.open:
+ self._next.append("do_subscription")
+ elif self.pre_confirmed:
+ # No confirmation is necessary. We can skip to seeing whether a
+ # moderator confirmation is necessary.
+ self._next.append("moderation_check")
+ else:
+ self._next.append("send_confirmation")
+
+ def _step_send_confirmation(self):
+ self._next.append("moderation_check")
+ self.save_state()
+ self._next.clear() # stop iteration until we get confirmation
+ # XXX: create the Pendable, send the ConfirmationNeededEvent
+ # (see Registrar.register)
+
+ def _step_moderation_check(self):
+ # Does the moderator need to approve the subscription request?
+ if not self.pre_approved and self.mlist.subscription_policy in (
+ SubscriptionPolicy.moderate,
+ SubscriptionPolicy.confirm_then_moderate):
+ self._next.append("get_moderator_approval")
+ else:
+ # The moderator does not need to approve the subscription, so go
+ # ahead and do that now.
+ self._next.append("do_subscription")
+
+ def _step_get_moderator_approval(self):
+ # In order to get the moderator's approval, we need to hold the
+ # subscription request in the database
+ request = RequestRecord(
+ self.address.email, self.subscriber.display_name,
+ DeliveryMode.regular, 'en')
+ hold_subscription(self.mlist, request)
+
+ def _step_do_subscription(self):
+ # We can immediately subscribe the user to the mailing list.
+ self.mlist.subscribe(self.subscriber)
+
+
@implementer(ISubscriptionService)
class SubscriptionService:
"""Subscription services for the REST API."""
@@ -168,7 +340,6 @@ class SubscriptionService:
delete_member(mlist, email, False, False)
-
def handle_ListDeletingEvent(event):
"""Delete a mailing list's members when the list is being deleted."""
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index 8ba5f52ff..b19dc5398 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -18,7 +18,8 @@
"""Tests for the subscription service."""
__all__ = [
- 'TestJoin'
+ 'TestJoin',
+ 'TestSubscriptionWorkflow',
]
@@ -26,11 +27,17 @@ import uuid
import unittest
from mailman.app.lifecycle import create_list
+from mailman.app.subscriptions import Workflow, SubscriptionWorkflow
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.member import MemberRole, MissingPreferredAddressError
+from mailman.interfaces.requests import IListRequests, RequestType
from mailman.interfaces.subscriptions import (
MissingUserError, ISubscriptionService)
from mailman.testing.layers import ConfigLayer
+from mailman.interfaces.mailinglist import SubscriptionPolicy
+from mailman.interfaces.usermanager import IUserManager
+from mailman.utilities.datetime import now
+from mock import Mock
from zope.component import getUtility
@@ -65,3 +72,118 @@ class TestJoin(unittest.TestCase):
self._service.join,
'test.example.com', anne.user.user_id,
role=MemberRole.owner)
+
+
+
+class TestWorkflow(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self.workflow = Workflow()
+ self.workflow._test_attribute = "test-value"
+ self.workflow._step_test = Mock()
+ self.workflow._next.append("test")
+
+ def test_iter_steps(self):
+ next(self.workflow)
+ self.assertTrue(self.workflow._step_test.called)
+ self.assertEqual(len(self.workflow._next), 0)
+ try:
+ next(self.workflow)
+ except StopIteration:
+ pass
+ else:
+ self.fail()
+
+ def test_save_restore(self):
+ self.workflow.save_state()
+ # Now create a new instance and restore
+ new_workflow = Workflow()
+ self.assertEqual(len(new_workflow._next), 0)
+ self.assertFalse(hasattr(new_workflow, "_test_attribute"))
+ new_workflow.restore_state()
+ self.assertEqual(len(new_workflow._next), 1)
+ self.assertEqual(new_workflow._next[0], "test")
+ self.assertEqual(self.workflow._test_attribute, "test-value")
+
+ def test_save_restore_no_next_step(self):
+ self.workflow._next.clear()
+ self.workflow.save_state()
+ # Now create a new instance and restore
+ new_workflow = Workflow()
+ new_workflow._next.append("test")
+ new_workflow.restore_state()
+ self.assertEqual(len(new_workflow._next), 0)
+
+
+
+class TestSubscriptionWorkflow(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._anne = 'anne@example.com'
+ self._user_manager = getUtility(IUserManager)
+
+ def test_preverified_address_joins_open_list(self):
+ # The mailing list has an open subscription policy, so the subscriber
+ # becomes a member with no human intervention.
+ self._mlist.subscription_policy = SubscriptionPolicy.open
+ anne = self._user_manager.create_address(self._anne, 'Anne Person')
+ self.assertIsNone(anne.verified_on)
+ self.assertIsNone(anne.user)
+ self.assertIsNone(self._mlist.subscribers.get_member(self._anne))
+ workflow = SubscriptionWorkflow(
+ self._mlist, anne,
+ pre_verified=True, pre_confirmed=False, pre_approved=False)
+ # Run the state machine to the end. The result is that her address
+ # will be verified, linked to a user, and subscribed to the mailing
+ # list.
+ list(workflow)
+ self.assertIsNotNone(anne.verified_on)
+ self.assertIsNotNone(anne.user)
+ self.assertIsNotNone(self._mlist.subscribers.get_member(self._anne))
+
+ def test_verified_address_joins_moderated_list(self):
+ # The mailing list is moderated but the subscriber is not a verified
+ # address and the subscription request is not pre-verified.
+ # A confirmation email must be sent, it will serve as the verification
+ # email too.
+ anne = self._user_manager.create_address(self._anne, 'Anne Person')
+ request_db = IListRequests(self._mlist)
+ def _do_check():
+ anne.verified_on = now()
+ self.assertIsNone(self._mlist.subscribers.get_member(self._anne))
+ workflow = SubscriptionWorkflow(
+ self._mlist, anne,
+ pre_verified=False, pre_confirmed=True, pre_approved=False)
+ # Run the state machine to the end.
+ list(workflow)
+ # Look in the requests db
+ requests = list(request_db.of_type(RequestType.subscription))
+ self.assertEqual(len(requests), 1)
+ self.assertEqual(requests[0].key, anne.email)
+ request_db.delete_request(requests[0].id)
+ self._mlist.subscription_policy = SubscriptionPolicy.moderate
+ _do_check()
+ self._mlist.subscription_policy = \
+ SubscriptionPolicy.confirm_then_moderate
+ _do_check()
+
+ def test_confirmation_required(self):
+ # Tests subscriptions where user confirmation is required
+ self._mlist.subscription_policy = \
+ SubscriptionPolicy.confirm_then_moderate
+ anne = self._user_manager.create_address(self._anne, 'Anne Person')
+ self.assertIsNone(self._mlist.subscribers.get_member(self._anne))
+ workflow = SubscriptionWorkflow(
+ self._mlist, anne,
+ pre_verified=True, pre_confirmed=False, pre_approved=True)
+ # Run the state machine to the end.
+ list(workflow)
+ # A confirmation request must be pending
+ # TODO: test it
+ # Now restore and re-run the state machine as if we got the confirmation
+ workflow.restore_state()
+ list(workflow)
+ self.assertIsNotNone(self._mlist.subscribers.get_member(self._anne))