diff options
Diffstat (limited to '')
| -rw-r--r-- | src/mailman/workflows/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/workflows/base.py (renamed from src/mailman/app/workflow.py) | 82 | ||||
| -rw-r--r-- | src/mailman/workflows/common.py | 401 | ||||
| -rw-r--r-- | src/mailman/workflows/subscription.py | 231 | ||||
| -rw-r--r-- | src/mailman/workflows/tests/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/workflows/tests/test_subscriptions.py (renamed from src/mailman/app/tests/test_subscriptions.py) | 240 | ||||
| -rw-r--r-- | src/mailman/workflows/tests/test_unsubscriptions.py (renamed from src/mailman/app/tests/test_unsubscriptions.py) | 152 | ||||
| -rw-r--r-- | src/mailman/workflows/tests/test_workflow.py (renamed from src/mailman/app/tests/test_workflow.py) | 14 | ||||
| -rw-r--r-- | src/mailman/workflows/unsubscription.py | 190 |
9 files changed, 1068 insertions, 242 deletions
diff --git a/src/mailman/workflows/__init__.py b/src/mailman/workflows/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/workflows/__init__.py diff --git a/src/mailman/app/workflow.py b/src/mailman/workflows/base.py index 92b78c184..8153bf77d 100644 --- a/src/mailman/app/workflow.py +++ b/src/mailman/workflows/base.py @@ -22,9 +22,12 @@ import json import logging from collections import deque -from mailman.interfaces.workflow import IWorkflowStateManager + +from mailman.interfaces.workflows import IWorkflow, IWorkflowStateManager +from mailman.utilities.modules import abstract_component from public import public from zope.component import getUtility +from zope.interface import implementer COMMASPACE = ', ' @@ -32,38 +35,27 @@ log = logging.getLogger('mailman.error') @public +@abstract_component +@implementer(IWorkflow) class Workflow: """Generic workflow.""" - SAVE_ATTRIBUTES = () - INITIAL_STATE = None + initial_state = None + save_attributes = () def __init__(self): self.token = None self._next = deque() - self.push(self.INITIAL_STATE) + self.push(self.initial_state) self.debug = False self._count = 0 - @property - def name(self): - return self.__class__.__name__ - def __iter__(self): + """See `IWorkflow`.""" return self - def push(self, step): - self._next.append(step) - - def _pop(self): - name = self._next.popleft() - step = getattr(self, '_step_{}'.format(name)) - self._count += 1 - if self.debug: # pragma: nocover - print('[{:02d}] -> {}'.format(self._count, name), file=sys.stderr) - return name, step - def __next__(self): + """See `IWorkflow`.""" try: name, step = self._pop() return step() @@ -73,13 +65,20 @@ class Workflow: log.exception('deque: {}'.format(COMMASPACE.join(self._next))) raise - def run_thru(self, stop_after): - """Run the state machine through and including the given step. + def push(self, step): + """See `IWorkflow`.""" + self._next.append(step) + + def _pop(self): + name = self._next.pop() + step = getattr(self, '_step_{}'.format(name)) + self._count += 1 + if self.debug: # pragma: nocover + print('[{:02d}] -> {}'.format(self._count, name), file=sys.stderr) + return name, step - :param stop_after: Name of method, sans prefix to run the - state machine through. In other words, the state machine runs - until the named method completes. - """ + def run_thru(self, stop_after): + """See `IWorkflow`.""" results = [] while True: try: @@ -93,12 +92,7 @@ class Workflow: return results def run_until(self, stop_before): - """Trun the state machine until (not including) the given step. - - :param stop_before: Name of method, sans prefix that the - state machine is run until the method is reached. Unlike - `run_thru()` the named method is not run. - """ + """See `IWorkflow`.""" results = [] while True: try: @@ -116,35 +110,29 @@ class Workflow: return results def save(self): + """See `IWorkflow`.""" assert self.token, 'Workflow token must be set' 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. This is not - # an issue in practice, 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() method. + data = {attr: getattr(self, attr) for attr in self.save_attributes} + # Save the workflow stack. if len(self._next) == 0: - step = None - elif len(self._next) == 1: - step = self._next[0] + steps = '[]' else: - raise AssertionError( - "Can't save a workflow state with more than one step " - "in the queue") - state_manager.save(self.token, step, json.dumps(data)) + steps = json.dumps(list(self._next)) + state_manager.save(self.token, steps, json.dumps(data)) def restore(self): + """See `IWorkflow`.""" state_manager = getUtility(IWorkflowStateManager) state = state_manager.restore(self.token) 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.steps: + self._next.extend(json.loads(state.steps)) data = json.loads(state.data) - for attr in self.SAVE_ATTRIBUTES: + for attr in self.save_attributes: try: setattr(self, attr, data[attr]) except KeyError: diff --git a/src/mailman/workflows/common.py b/src/mailman/workflows/common.py new file mode 100644 index 000000000..c250785c2 --- /dev/null +++ b/src/mailman/workflows/common.py @@ -0,0 +1,401 @@ +# Copyright (C) 2015-2017 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/>. + +"""Common support between subscription and unsubscription.""" + +import uuid +import logging + +from datetime import timedelta +from email.utils import formataddr +from enum import Enum +from itertools import chain + +from mailman.app.membership import delete_member +from mailman.config import config +from mailman.core.i18n import _ +from mailman.email.message import UserNotification +from mailman.interfaces.address import IAddress +from mailman.interfaces.bans import IBanManager +from mailman.interfaces.member import (AlreadySubscribedError, MemberRole, + MembershipIsBannedError, + NotAMemberError) +from mailman.interfaces.pending import IPendable, IPendings +from mailman.interfaces.subscriptions import (SubscriptionPendingError, + TokenOwner) +from mailman.interfaces.template import ITemplateLoader +from mailman.interfaces.user import IUser +from mailman.interfaces.usermanager import IUserManager +from mailman.interfaces.workflows import ISubscriptionWorkflow +from mailman.utilities.datetime import now +from mailman.utilities.string import expand, wrap +from mailman.workflows.base import Workflow +from zope.component import getUtility +from zope.interface import implementer + + +log = logging.getLogger('mailman.subscribe') + + +class WhichSubscriber(Enum): + address = 1 + user = 2 + + +class WhichWorkflow(Enum): + subscription = 1 + unsubscription = 2 + + +@implementer(IPendable) +class PendableSubscription(dict): + PEND_TYPE = 'subscription' + + +@implementer(IPendable) +class PendableUnsubscription(dict): + PEND_TYPE = 'unsubscription' + + +class SubscriptionWorkflowCommon(Workflow): + """Common support between subscription and unsubscription.""" + + def __init__(self, mlist, subscriber): + super().__init__() + self.mlist = mlist + self.address = None + self.user = None + self.which = None + self.member = None + self._set_token(TokenOwner.no_one) + # The subscriber must be either an IUser or IAddress. + if IAddress.providedBy(subscriber): + self.address = subscriber + self.user = self.address.user + self.which = WhichSubscriber.address + elif IUser.providedBy(subscriber): + self.address = subscriber.preferred_address + self.user = subscriber + self.which = WhichSubscriber.user + self.subscriber = subscriber + + @property + def user_key(self): + # For save. + return self.user.user_id.hex + + @user_key.setter + def user_key(self, hex_key): + # For restore. + uid = uuid.UUID(hex_key) + self.user = getUtility(IUserManager).get_user_by_id(uid) + if self.user is None: + self.user = self.address.user + + @property + def address_key(self): + # For save. + return self.address.email + + @address_key.setter + def address_key(self, email): + # For restore. + self.address = getUtility(IUserManager).get_address(email) + assert self.address is not None + + @property + def subscriber_key(self): + return self.which.value + + @subscriber_key.setter + def subscriber_key(self, key): + self.which = WhichSubscriber(key) + + @property + def token_owner_key(self): + return self.token_owner.value + + @token_owner_key.setter + def token_owner_key(self, value): + self.token_owner = TokenOwner(value) + + def _restore_subscriber(self): + # Restore a little extra state that can't be stored in the database + # (because the order of setattr() on restore is indeterminate), then + # continue with the confirmation/verification step. + if self.which is WhichSubscriber.address: + self.subscriber = self.address + else: + assert self.which is WhichSubscriber.user + self.subscriber = self.user + + def _set_token(self, token_owner): + assert isinstance(token_owner, TokenOwner) + pendings = getUtility(IPendings) + # Clear out the previous pending token if there is one. + if self.token is not None: + pendings.confirm(self.token) + # Create a new token to prevent replay attacks. It seems like this + # would produce the same token, but it won't because the pending adds a + # bit of randomization. + self.token_owner = token_owner + if token_owner is TokenOwner.no_one: + self.token = None + return + + pendable = self.pendable_class()( + list_id=self.mlist.list_id, + email=self.address.email, + display_name=self.address.display_name, + when=now().replace(microsecond=0).isoformat(), + token_owner=token_owner.name, + ) + self.token = pendings.add(pendable, timedelta(days=3650)) + + @classmethod + def pendable_class(cls): + @implementer(IPendable) + class Pendable(dict): + PEND_TYPE = cls.name + return Pendable + + +class SubscriptionBase(SubscriptionWorkflowCommon): + + def __init__(self, mlist, subscriber): + super().__init__(mlist, subscriber) + self._workflow = WhichWorkflow.subscription + + def _step_sanity_checks(self): + # Ensure that we have both an address and a user, even if the address + # is not verified. We can't set the preferred address until it is + # verified. + 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) + if self.address is None: + assert self.user.preferred_address is None, ( + "Preferred address exists, but wasn't used in constructor") + addresses = list(self.user.addresses) + if len(addresses) == 0: + raise AssertionError('User has no addresses: {}'.format( + self.user)) + # This is rather arbitrary, but we have no choice. + self.address = addresses[0] + assert self.user is not None and self.address is not None, ( + 'Insane sanity check results') + # Is this subscriber already a member? + if (self.which is WhichSubscriber.user and + self.user.preferred_address is not None): + subscriber = self.user + else: + subscriber = self.address + if self.mlist.is_subscribed(subscriber): + # 2017-04-22 BAW: This branch actually *does* get covered, as I've + # verified by a full coverage run, but diffcov for some reason + # claims that the test added in the branch that added this code + # does not cover the change. That seems like a bug in diffcov. + raise AlreadySubscribedError( # pragma: nocover + self.mlist.fqdn_listname, + self.address.email, + MemberRole.member) + # Is this email address banned? + if IBanManager(self.mlist).is_banned(self.address.email): + raise MembershipIsBannedError(self.mlist, self.address.email) + # Check if there is already a subscription request for this email. + # Look at all known subscription workflows, because any pending + # subscription workflow is exclusive. + sub_workflows = [workflow_class + for workflow_class in config.workflows.values() + if ISubscriptionWorkflow.implementedBy(workflow_class) + ] + generators = [getUtility(IPendings).find(mlist=self.mlist, + pend_type=sub_workflow.name) + for + sub_workflow in sub_workflows] + pendings = chain.from_iterable(generators) + for token, pendable in pendings: + if pendable['email'] == self.address.email: + raise SubscriptionPendingError(self.mlist, self.address.email) + # Start out with the subscriber being the token owner. + + def _step_do_subscription(self): + # We can immediately subscribe the user to the mailing list. + self.member = self.mlist.subscribe(self.subscriber) + assert self.token is None and self.token_owner is TokenOwner.no_one, ( + 'Unexpected active token at end of subscription workflow') + + +class UnsubscriptionBase(SubscriptionWorkflowCommon): + + def __init__(self, mlist, subscriber): + super().__init__(mlist, subscriber) + if IAddress.providedBy(subscriber) or IUser.providedBy(subscriber): + self.member = self.mlist.regular_members.get_member( + self.address.email) + self._workflow = WhichWorkflow.unsubscription + + def _step_subscription_checks(self): + assert self.mlist.is_subscribed(self.subscriber) + + def _step_do_unsubscription(self): + try: + delete_member(self.mlist, self.address.email) + except NotAMemberError: + # The member has already been unsubscribed. + pass + self.member = None + assert self.token is None and self.token_owner is TokenOwner.no_one, ( + 'Unexpected active token at end of subscription workflow') + + +class RequestMixin: + + def _step_send_confirmation(self): + self._set_token(TokenOwner.subscriber) + self.push('do_confirm_verify') + self.save() + if self._workflow is WhichWorkflow.subscription: + template_name = 'list:user:action:subscribe' + else: + template_name = 'list:user:action:unsubscribe' + + subject = 'confirm {}'.format(self.token) + confirm_address = self.mlist.confirm_address(self.token) + email_address = self.address.email + # Send a verification email to the address. + template = getUtility(ITemplateLoader).get(template_name, self.mlist) + text = expand(template, self.mlist, dict( + token=self.token, + subject=subject, + confirm_email=confirm_address, + user_email=email_address, + # For backward compatibility. + confirm_address=confirm_address, + email_address=email_address, + domain_name=self.mlist.domain.mail_host, + contact_address=self.mlist.owner_address, + )) + msg = UserNotification(email_address, confirm_address, subject, text) + msg.send(self.mlist, add_precedence=False) + # Now we wait for the confirmation. + raise StopIteration + + def _step_do_confirm_verify(self): + # Restore a little extra state that can't be stored in the database. + self._restore_subscriber() + # Reset the token so it can't be used in a replay attack. + self._set_token(TokenOwner.no_one) + # The user has confirmed their subscription request, and also verified + # their email address if necessary. This latter needs to be set on the + # IAddress, but there's nothing more to do about the confirmation step. + # We just continue along with the workflow. + if self.address.verified_on is None: + self.address.verified_on = now() + self.verified = True + self.confirmed = True + + if self._workflow is WhichWorkflow.unsubscription: + self.member = self.mlist.regular_members.get_member( + self.address.email) + + +class VerificationMixin(RequestMixin): + + def __init__(self, pre_verified=False): + self.verified = pre_verified + + def _step_verification_checks(self): + # Is the address already verified, or is the pre-verified flag set? + if self.address.verified_on is None: + if self.verified: + self.address.verified_on = now() + else: + # The address being subscribed is not yet verified, so we need + # to send a validation email that will also confirm that the + # user wants to be subscribed to this mailing list. + self.push('send_confirmation') + + +class ConfirmationMixin(RequestMixin): + + def __init__(self, pre_confirmed=False): + self.confirmed = pre_confirmed + + def _step_confirmation_checks(self): + # If the subscription has been pre-confirmed, then we can skip the + # confirmation check. + if not self.confirmed: + # The user must confirm their subscription. + self.push('send_confirmation') + + +class ModerationMixin: + + def __init__(self, pre_approved=False): + self.approved = pre_approved + + def _step_moderation_checks(self): + # Does the moderator need to approve the request? + if not self.approved: + self.push('get_moderator_approval') + + def _step_get_moderator_approval(self): + # Here's the next step in the workflow, assuming the moderator + # approves of the request. If they don't, the workflow and + # request will just be thrown away. + self._set_token(TokenOwner.moderator) + self.push('restore') + self.save() + + if self._workflow is WhichWorkflow.subscription: + workflow_name = 'subscription' + template_name = 'list:admin:action:subscribe' + else: + workflow_name = 'unsubscription' + template_name = 'list:admin:action:unsubscribe' + + log.info('{}: held {} request from {}'.format( + self.mlist.fqdn_listname, workflow_name, self.address.email)) + # Possibly send a notification to the list moderators. + if self.mlist.admin_immed_notify: + subject = _( + 'New $workflow_name request to $self.mlist.display_name ' + 'from $self.address.email') + username = formataddr( + (self.subscriber.display_name, self.address.email)) + template = getUtility(ITemplateLoader).get( + template_name, self.mlist) + text = wrap(expand(template, self.mlist, dict( + member=username, + ))) + # This message should appear to come from the <list>-owner so as + # to avoid any useless bounce processing. + msg = UserNotification( + self.mlist.owner_address, self.mlist.owner_address, + subject, text, self.mlist.preferred_language) + msg.send(self.mlist) + # The workflow must stop running here. + raise StopIteration + + def _step_restore(self): + # Prevent replay attacks. + self._set_token(TokenOwner.no_one) + # Restore a little extra state that can't be stored in the database. + self._restore_subscriber() diff --git a/src/mailman/workflows/subscription.py b/src/mailman/workflows/subscription.py new file mode 100644 index 000000000..f780c96a0 --- /dev/null +++ b/src/mailman/workflows/subscription.py @@ -0,0 +1,231 @@ +# Copyright (C) 2015-2017 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/>. + +"""""" + +from mailman.core.i18n import _ +from mailman.interfaces.workflows import ISubscriptionWorkflow +from mailman.workflows.common import (ConfirmationMixin, ModerationMixin, + SubscriptionBase, VerificationMixin) +from public import public +from zope.interface import implementer + + +@public +@implementer(ISubscriptionWorkflow) +class OpenSubscriptionPolicy(SubscriptionBase, VerificationMixin): + """""" + + name = 'sub-policy-open' + description = _('An open subscription policy, only requires verification.') + initial_state = 'prepare' + save_attributes = ( + 'verified', + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None, *, + pre_verified=False): + """ + + :param mlist: + :param subscriber: The user or address to subscribe. + :type subscriber: ``IUser`` or ``IAddress`` + :param pre_verified: A flag indicating whether the subscriber's email + address should be considered pre-verified. Normally a never + before seen email address must be verified by mail-back + confirmation. Setting this flag to True automatically verifies + such addresses without the mail-back. (A confirmation message may + still be sent under other conditions.) + :type pre_verified: bool + """ + SubscriptionBase.__init__(self, mlist, subscriber) + VerificationMixin.__init__(self, pre_verified=pre_verified) + + def _step_prepare(self): + self.push('do_subscription') + self.push('verification_checks') + self.push('sanity_checks') + + +@public +@implementer(ISubscriptionWorkflow) +class ConfirmSubscriptionPolicy(SubscriptionBase, ConfirmationMixin, + VerificationMixin): + """""" + + name = 'sub-policy-confirm' + description = _('An subscription policy that requires confirmation.') + initial_state = 'prepare' + save_attributes = ( + 'verified', + 'confirmed', + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None, *, + pre_verified=False, pre_confirmed=False): + """ + + :param mlist: + :param subscriber: The user or address to subscribe. + :type subscriber: ``IUser`` or ``IAddress`` + :param pre_verified: A flag indicating whether the subscriber's email + address should be considered pre-verified. Normally a never + before seen email address must be verified by mail-back + confirmation. Setting this flag to True automatically verifies + such addresses without the mail-back. (A confirmation message may + still be sent under other conditions.) + :type pre_verified: bool + :param pre_confirmed: A flag indicating whether, when required by the + subscription policy, a subscription request should be considered + pre-confirmed. Normally in such cases, a mail-back confirmation + message is sent to the subscriber, which must be positively + acknowledged by some manner. Setting this flag to True + automatically confirms the subscription request. (A confirmation + message may still be sent under other conditions.) + :type pre_confirmed: bool + """ + SubscriptionBase.__init__(self, mlist, subscriber) + VerificationMixin.__init__(self, pre_verified=pre_verified) + ConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) + + def _step_prepare(self): + self.push('do_subscription') + self.push('confirmation_checks') + self.push('verification_checks') + self.push('sanity_checks') + + +@public +@implementer(ISubscriptionWorkflow) +class ModerationSubscriptionPolicy(SubscriptionBase, ModerationMixin, + VerificationMixin): + """""" + + name = 'sub-policy-moderate' + description = _('A subscription policy that requires moderation.') + initial_state = 'prepare' + save_attributes = ( + 'approved', + 'verified', + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None, *, + pre_verified=False, pre_approved=False): + """ + + :param mlist: + :param subscriber: The user or address to subscribe. + :type subscriber: ``IUser`` or ``IAddress`` + :param pre_verified: A flag indicating whether the subscriber's email + address should be considered pre-verified. Normally a never + before seen email address must be verified by mail-back + confirmation. Setting this flag to True automatically verifies + such addresses without the mail-back. (A confirmation message may + still be sent under other conditions.) + :type pre_verified: bool + :param pre_approved: A flag indicating whether, when required by the + subscription policy, a subscription request should be considered + pre-approved. Normally in such cases, the list administrator is + notified that an approval is necessary, which must be positively + acknowledged in some manner. Setting this flag to True + automatically approves the subscription request. + :type pre_approved: bool + """ + SubscriptionBase.__init__(self, mlist, subscriber) + VerificationMixin.__init__(self, pre_verified=pre_verified) + ModerationMixin.__init__(self, pre_approved=pre_approved) + + def _step_prepare(self): + self.push('do_subscription') + self.push('moderation_checks') + self.push('verification_checks') + self.push('sanity_checks') + + +@public +@implementer(ISubscriptionWorkflow) +class ConfirmModerationSubscriptionPolicy(SubscriptionBase, ConfirmationMixin, + ModerationMixin, VerificationMixin): + """""" + + name = 'sub-policy-confirm-moderate' + description = _( + 'A subscription policy that requires moderation after confirmation.') + initial_state = 'prepare' + save_attributes = ( + 'approved', + 'confirmed', + 'verified', + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None, *, + pre_verified=False, pre_confirmed=False, pre_approved=False): + """ + + :param mlist: + :param subscriber: The user or address to subscribe. + :type subscriber: ``IUser`` or ``IAddress`` + :param pre_verified: A flag indicating whether the subscriber's email + address should be considered pre-verified. Normally a never + before seen email address must be verified by mail-back + confirmation. Setting this flag to True automatically verifies + such addresses without the mail-back. (A confirmation message may + still be sent under other conditions.) + :type pre_verified: bool + :param pre_confirmed: A flag indicating whether, when required by the + subscription policy, a subscription request should be considered + pre-confirmed. Normally in such cases, a mail-back confirmation + message is sent to the subscriber, which must be positively + acknowledged by some manner. Setting this flag to True + automatically confirms the subscription request. (A confirmation + message may still be sent under other conditions.) + :type pre_confirmed: bool + :param pre_approved: A flag indicating whether, when required by the + subscription policy, a subscription request should be considered + pre-approved. Normally in such cases, the list administrator is + notified that an approval is necessary, which must be positively + acknowledged in some manner. Setting this flag to True + automatically approves the subscription request. + :type pre_approved: bool + """ + SubscriptionBase.__init__(self, mlist, subscriber) + VerificationMixin.__init__(self, pre_verified=pre_verified) + ConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) + ModerationMixin.__init__(self, pre_approved=pre_approved) + + def _step_prepare(self): + self.push('do_subscription') + self.push('moderation_checks') + self.push('confirmation_checks') + self.push('verification_checks') + self.push('sanity_checks') diff --git a/src/mailman/workflows/tests/__init__.py b/src/mailman/workflows/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/workflows/tests/__init__.py diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/workflows/tests/test_subscriptions.py index 0519ec385..65569691b 100644 --- a/src/mailman/app/tests/test_subscriptions.py +++ b/src/mailman/workflows/tests/test_subscriptions.py @@ -21,17 +21,20 @@ import unittest from contextlib import suppress from mailman.app.lifecycle import create_list -from mailman.app.subscriptions import SubscriptionWorkflow from mailman.interfaces.bans import IBanManager -from mailman.interfaces.mailinglist import SubscriptionPolicy from mailman.interfaces.member import MemberRole, MembershipIsBannedError from mailman.interfaces.pending import IPendings -from mailman.interfaces.subscriptions import TokenOwner +from mailman.interfaces.subscriptions import ( + SubscriptionPendingError, + TokenOwner) from mailman.interfaces.usermanager import IUserManager from mailman.testing.helpers import ( LogFileMark, get_queue_messages, set_preferred) from mailman.testing.layers import ConfigLayer from mailman.utilities.datetime import now +from mailman.workflows.subscription import ( + ConfirmModerationSubscriptionPolicy, ConfirmSubscriptionPolicy, + ModerationSubscriptionPolicy, OpenSubscriptionPolicy) from unittest.mock import patch from zope.component import getUtility @@ -55,7 +58,7 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_start_state(self): # The workflow starts with no tokens or member. - workflow = SubscriptionWorkflow(self._mlist) + workflow = ConfirmSubscriptionPolicy(self._mlist) self.assertIsNone(workflow.token) self.assertEqual(workflow.token_owner, TokenOwner.no_one) self.assertIsNone(workflow.member) @@ -64,7 +67,7 @@ class TestSubscriptionWorkflow(unittest.TestCase): # There is a Pendable associated with the held request, and it has # some data associated with it. anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) with suppress(StopIteration): workflow.run_thru('send_confirmation') self.assertIsNotNone(workflow.token) @@ -79,14 +82,14 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_user_or_address_required(self): # The `subscriber` attribute must be a user or address. - workflow = SubscriptionWorkflow(self._mlist) + workflow = ConfirmSubscriptionPolicy(self._mlist) self.assertRaises(AssertionError, list, workflow) def test_sanity_checks_address(self): # Ensure that the sanity check phase, when given an IAddress, ends up # with a linked user. anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) self.assertIsNotNone(workflow.address) self.assertIsNone(workflow.user) workflow.run_thru('sanity_checks') @@ -99,7 +102,7 @@ class TestSubscriptionWorkflow(unittest.TestCase): # preferred address, ends up with an address. anne = self._user_manager.make_user(self._anne) address = set_preferred(anne) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) # The constructor sets workflow.address because the user has a # preferred address. self.assertEqual(workflow.address, address) @@ -113,7 +116,7 @@ class TestSubscriptionWorkflow(unittest.TestCase): # preferred address, but with at least one linked address, gets an # address. anne = self._user_manager.make_user(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) self.assertIsNone(workflow.address) self.assertEqual(workflow.user, anne) workflow.run_thru('sanity_checks') @@ -127,7 +130,7 @@ class TestSubscriptionWorkflow(unittest.TestCase): anne = self._user_manager.make_user(self._anne) anne.link(self._user_manager.create_address('anne@example.net')) anne.link(self._user_manager.create_address('anne@example.org')) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) self.assertIsNone(workflow.address) self.assertEqual(workflow.user, anne) workflow.run_thru('sanity_checks') @@ -139,29 +142,39 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_sanity_checks_user_without_addresses(self): # It is an error to try to subscribe a user with no linked addresses. user = self._user_manager.create_user() - workflow = SubscriptionWorkflow(self._mlist, user) + workflow = ConfirmSubscriptionPolicy(self._mlist, user) self.assertRaises(AssertionError, workflow.run_thru, 'sanity_checks') def test_sanity_checks_globally_banned_address(self): # An exception is raised if the address is globally banned. anne = self._user_manager.create_address(self._anne) IBanManager(None).ban(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) self.assertRaises(MembershipIsBannedError, list, workflow) def test_sanity_checks_banned_address(self): # An exception is raised if the address is banned by the mailing list. anne = self._user_manager.create_address(self._anne) IBanManager(self._mlist).ban(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) self.assertRaises(MembershipIsBannedError, list, workflow) + def test_sanity_checks_already_requested(self): + # An exception is raised if there is already a subscription request. + anne = self._user_manager.create_address(self._anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) + list(workflow) + other_workflow = ConfirmSubscriptionPolicy(self._mlist, anne) + self.assertRaises(SubscriptionPendingError, list, other_workflow) + # The original workflow token is still in the database. + self._expected_pendings_count = 1 + def test_verification_checks_with_verified_address(self): # When the address is already verified, we skip straight to the # confirmation checks. anne = self._user_manager.create_address(self._anne) anne.verified_on = now() - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) workflow.run_thru('verification_checks') with patch.object(workflow, '_step_confirmation_checks') as step: next(workflow) @@ -171,7 +184,8 @@ class TestSubscriptionWorkflow(unittest.TestCase): # When the address is not yet verified, but the pre-verified flag is # passed to the workflow, we skip to the confirmation checks. anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne, + pre_verified=True) workflow.run_thru('verification_checks') with patch.object(workflow, '_step_confirmation_checks') as step: next(workflow) @@ -184,7 +198,7 @@ class TestSubscriptionWorkflow(unittest.TestCase): # A confirmation message must be sent to the user which will also # verify their address. anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = ConfirmSubscriptionPolicy(self._mlist, anne) workflow.run_thru('verification_checks') with patch.object(workflow, '_step_send_confirmation') as step: next(workflow) @@ -195,10 +209,11 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_confirmation_checks_open_list(self): # A subscription to an open list does not need to be confirmed or # moderated. - self._mlist.subscription_policy = SubscriptionPolicy.open + self._mlist.subscription_policy = OpenSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) - workflow.run_thru('confirmation_checks') + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) + workflow.run_thru('verification_checks') with patch.object(workflow, '_step_do_subscription') as step: next(workflow) step.assert_called_once_with() @@ -206,10 +221,11 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_confirmation_checks_no_user_confirmation_needed(self): # A subscription to a list which does not need user confirmation skips # to the moderation checks. - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) - workflow.run_thru('confirmation_checks') + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) + workflow.run_thru('verification_checks') with patch.object(workflow, '_step_moderation_checks') as step: next(workflow) step.assert_called_once_with() @@ -218,11 +234,11 @@ class TestSubscriptionWorkflow(unittest.TestCase): # The subscription policy requires user confirmation, but their # subscription is pre-confirmed. Since moderation is not required, # the user will be immediately subscribed. - self._mlist.subscription_policy = SubscriptionPolicy.confirm + self._mlist.subscription_policy = ConfirmSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True, + pre_confirmed=True) workflow.run_thru('confirmation_checks') with patch.object(workflow, '_step_do_subscription') as step: next(workflow) @@ -233,11 +249,11 @@ class TestSubscriptionWorkflow(unittest.TestCase): # subscription is pre-confirmed. Since moderation is required, that # check will be performed. self._mlist.subscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) + ConfirmModerationSubscriptionPolicy) anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True, + pre_confirmed=True) workflow.run_thru('confirmation_checks') with patch.object(workflow, '_step_moderation_checks') as step: next(workflow) @@ -247,11 +263,11 @@ class TestSubscriptionWorkflow(unittest.TestCase): # The subscription policy requires user confirmation and moderation, # but their subscription is pre-confirmed. self._mlist.subscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) + ConfirmModerationSubscriptionPolicy) anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True, + pre_confirmed=True) workflow.run_thru('confirmation_checks') with patch.object(workflow, '_step_moderation_checks') as step: next(workflow) @@ -260,9 +276,10 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_confirmation_checks_confirmation_needed(self): # The subscription policy requires confirmation and the subscription # is not pre-confirmed. - self._mlist.subscription_policy = SubscriptionPolicy.confirm + self._mlist.subscription_policy = ConfirmSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) workflow.run_thru('confirmation_checks') with patch.object(workflow, '_step_send_confirmation') as step: next(workflow) @@ -272,9 +289,10 @@ class TestSubscriptionWorkflow(unittest.TestCase): # The subscription policy requires confirmation and moderation, and the # subscription is not pre-confirmed. self._mlist.subscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) + ConfirmModerationSubscriptionPolicy) anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) workflow.run_thru('confirmation_checks') with patch.object(workflow, '_step_send_confirmation') as step: next(workflow) @@ -282,11 +300,11 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_moderation_checks_pre_approved(self): # The subscription is pre-approved by the moderator. - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_approved=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True, + pre_approved=True) workflow.run_thru('moderation_checks') with patch.object(workflow, '_step_do_subscription') as step: next(workflow) @@ -294,9 +312,10 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_moderation_checks_approval_required(self): # The moderator must approve the subscription. - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) workflow.run_thru('moderation_checks') with patch.object(workflow, '_step_get_moderator_approval') as step: next(workflow) @@ -306,9 +325,10 @@ class TestSubscriptionWorkflow(unittest.TestCase): # An open subscription policy plus a pre-verified address means the # user gets subscribed to the mailing list without any further # confirmations or approvals. - self._mlist.subscription_policy = SubscriptionPolicy.open + self._mlist.subscription_policy = OpenSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) # Consume the entire state machine. list(workflow) # Anne is now a member of the mailing list. @@ -323,11 +343,11 @@ class TestSubscriptionWorkflow(unittest.TestCase): # An moderation-requiring subscription policy plus a pre-verified and # pre-approved address means the user gets subscribed to the mailing # list without any further confirmations or approvals. - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_approved=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True, + pre_approved=True) # Consume the entire state machine. list(workflow) # Anne is now a member of the mailing list. @@ -343,12 +363,12 @@ class TestSubscriptionWorkflow(unittest.TestCase): # pre-approved address means the user gets subscribed to the mailing # list without any further confirmations or approvals. self._mlist.subscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) + ConfirmModerationSubscriptionPolicy) anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True, - pre_approved=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True, + pre_confirmed=True, + pre_approved=True) # Consume the entire state machine. list(workflow) # Anne is now a member of the mailing list. @@ -362,12 +382,10 @@ class TestSubscriptionWorkflow(unittest.TestCase): def test_do_subscription_cleanups(self): # Once the user is subscribed, the token, and its associated pending # database record will be removed from the database. - self._mlist.subscription_policy = SubscriptionPolicy.open + self._mlist.subscription_policy = OpenSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True, - pre_approved=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) # Consume the entire state machine. list(workflow) # Anne is now a member of the mailing list. @@ -382,11 +400,10 @@ class TestSubscriptionWorkflow(unittest.TestCase): # The workflow runs until moderator approval is required, at which # point the workflow is saved. Once the moderator approves, the # workflow resumes and the user is subscribed. - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) # Consume the entire state machine. list(workflow) # The user is not currently subscribed to the mailing list. @@ -399,7 +416,7 @@ class TestSubscriptionWorkflow(unittest.TestCase): # Create a new workflow with the previous workflow's save token, and # restore its state. This models an approved subscription and should # result in the user getting subscribed. - approved_workflow = SubscriptionWorkflow(self._mlist) + approved_workflow = self._mlist.subscription_policy(self._mlist) approved_workflow.token = workflow.token approved_workflow.restore() list(approved_workflow) @@ -415,11 +432,10 @@ class TestSubscriptionWorkflow(unittest.TestCase): # When the subscription is held for moderator approval, a message is # logged. mark = LogFileMark('mailman.subscribe') - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) # Consume the entire state machine. list(workflow) self.assertIn( @@ -434,14 +450,13 @@ class TestSubscriptionWorkflow(unittest.TestCase): # When the subscription is held for moderator approval, and the list # is so configured, a notification is sent to the list moderators. self._mlist.admin_immed_notify = True - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) bart = self._user_manager.create_user('bart@example.com', 'Bart User') address = set_preferred(bart) self._mlist.subscribe(address, MemberRole.moderator) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) # Consume the entire state machine. list(workflow) # Find the moderator message. @@ -452,13 +467,13 @@ class TestSubscriptionWorkflow(unittest.TestCase): else: raise AssertionError('No moderator email found') self.assertEqual( - item.msgdata['recipients'], {'test-owner@example.com'}) + item.msgdata['recipients'], {'test-owner@example.com'}) message = items[0].msg self.assertEqual(message['From'], 'test-owner@example.com') self.assertEqual(message['To'], 'test-owner@example.com') self.assertEqual( - message['Subject'], - 'New subscription request to Test from anne@example.com') + message['Subject'], + 'New subscription request to Test from anne@example.com') self.assertEqual(message.get_payload(), """\ Your authorization is required for a mailing list subscription request approval: @@ -474,11 +489,10 @@ approval: # When the subscription is held for moderator approval, and the list # is so configured, a notification is sent to the list moderators. self._mlist.admin_immed_notify = False - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, - pre_verified=True, - pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) # Consume the entire state machine. list(workflow) get_queue_messages('virgin', expected_count=0) @@ -491,14 +505,14 @@ approval: anne = self._user_manager.create_address(self._anne) self.assertIsNone(anne.verified_on) # Run the workflow to model the confirmation step. - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = self._mlist.subscription_policy(self._mlist, anne) list(workflow) items = get_queue_messages('virgin', expected_count=1) message = items[0].msg token = workflow.token self.assertEqual(message['Subject'], 'confirm {}'.format(token)) self.assertEqual( - message['From'], 'test-confirm+{}@example.com'.format(token)) + message['From'], 'test-confirm+{}@example.com'.format(token)) # The confirmation message is not `Precedence: bulk`. self.assertIsNone(message['precedence']) # The state machine stopped at the moderator approval so there will be @@ -511,15 +525,16 @@ approval: anne = self._user_manager.create_address(self._anne) self.assertIsNone(anne.verified_on) # Run the workflow to model the confirmation step. - workflow = SubscriptionWorkflow(self._mlist, anne, pre_confirmed=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_confirmed=True) list(workflow) items = get_queue_messages('virgin', expected_count=1) message = items[0].msg token = workflow.token self.assertEqual( - message['Subject'], 'confirm {}'.format(workflow.token)) + message['Subject'], 'confirm {}'.format(workflow.token)) self.assertEqual( - message['From'], 'test-confirm+{}@example.com'.format(token)) + message['From'], 'test-confirm+{}@example.com'.format(token)) # The state machine stopped at the moderator approval so there will be # one token still in the database. self._expected_pendings_count = 1 @@ -527,19 +542,20 @@ approval: def test_send_confirmation_pre_verified(self): # A confirmation message gets sent even when the address is verified # when the subscription must be confirmed. - self._mlist.subscription_policy = SubscriptionPolicy.confirm + self._mlist.subscription_policy = ConfirmSubscriptionPolicy anne = self._user_manager.create_address(self._anne) self.assertIsNone(anne.verified_on) # Run the workflow to model the confirmation step. - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) list(workflow) items = get_queue_messages('virgin', expected_count=1) message = items[0].msg token = workflow.token self.assertEqual( - message['Subject'], 'confirm {}'.format(workflow.token)) + message['Subject'], 'confirm {}'.format(workflow.token)) self.assertEqual( - message['From'], 'test-confirm+{}@example.com'.format(token)) + message['From'], 'test-confirm+{}@example.com'.format(token)) # The state machine stopped at the moderator approval so there will be # one token still in the database. self._expected_pendings_count = 1 @@ -551,11 +567,11 @@ approval: anne = self._user_manager.create_address(self._anne) self.assertIsNone(anne.verified_on) # Run the workflow to model the confirmation step. - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = self._mlist.subscription_policy(self._mlist, anne) list(workflow) # The address is still not verified. self.assertIsNone(anne.verified_on) - confirm_workflow = SubscriptionWorkflow(self._mlist) + confirm_workflow = self._mlist.subscription_policy(self._mlist) confirm_workflow.token = workflow.token confirm_workflow.restore() confirm_workflow.run_thru('do_confirm_verify') @@ -569,11 +585,11 @@ approval: set_preferred(anne) # Run the workflow to model the confirmation step. There is no # subscriber attribute yet. - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = self._mlist.subscription_policy(self._mlist, anne) list(workflow) self.assertEqual(workflow.subscriber, anne) # Do a confirmation workflow, which should now set the subscriber. - confirm_workflow = SubscriptionWorkflow(self._mlist) + confirm_workflow = self._mlist.subscription_policy(self._mlist) confirm_workflow.token = workflow.token confirm_workflow.restore() confirm_workflow.run_thru('do_confirm_verify') @@ -584,10 +600,10 @@ approval: # Subscriptions to the mailing list must be confirmed. Once that's # done, the user's address (which is not initially verified) gets # subscribed to the mailing list. - self._mlist.subscription_policy = SubscriptionPolicy.confirm + self._mlist.subscription_policy = ConfirmSubscriptionPolicy anne = self._user_manager.create_address(self._anne) self.assertIsNone(anne.verified_on) - workflow = SubscriptionWorkflow(self._mlist, anne) + workflow = self._mlist.subscription_policy(self._mlist, anne) list(workflow) # Anne is not yet a member. member = self._mlist.regular_members.get_member(self._anne) @@ -597,7 +613,7 @@ approval: self.assertIsNotNone(workflow.token) self.assertEqual(workflow.token_owner, TokenOwner.subscriber) # Confirm. - confirm_workflow = SubscriptionWorkflow(self._mlist) + confirm_workflow = self._mlist.subscription_policy(self._mlist) confirm_workflow.token = workflow.token confirm_workflow.restore() list(confirm_workflow) @@ -615,9 +631,10 @@ approval: # the user confirming their subscription, and then the moderator # approving it, that different tokens are used in these two cases. self._mlist.subscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) + ConfirmModerationSubscriptionPolicy) anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) # Run the state machine up to the first confirmation, and cache the # confirmation token. list(workflow) @@ -630,7 +647,7 @@ approval: self.assertIsNotNone(workflow.token) self.assertEqual(workflow.token_owner, TokenOwner.subscriber) # The old token will not work for moderator approval. - moderator_workflow = SubscriptionWorkflow(self._mlist) + moderator_workflow = self._mlist.subscription_policy(self._mlist) moderator_workflow.token = token moderator_workflow.restore() list(moderator_workflow) @@ -641,7 +658,7 @@ approval: # that there's a new token for the next steps. self.assertNotEqual(token, moderator_workflow.token) # The old token won't work. - final_workflow = SubscriptionWorkflow(self._mlist) + final_workflow = self._mlist.subscription_policy(self._mlist) final_workflow.token = token self.assertRaises(LookupError, final_workflow.restore) # Running this workflow will fail. @@ -666,11 +683,11 @@ approval: def test_confirmation_needed_and_pre_confirmed(self): # The subscription policy is 'confirm' but the subscription is # pre-confirmed so the moderation checks can be skipped. - self._mlist.subscription_policy = SubscriptionPolicy.confirm + self._mlist.subscription_policy = ConfirmSubscriptionPolicy anne = self._user_manager.create_address(self._anne) - workflow = SubscriptionWorkflow( - self._mlist, anne, - pre_verified=True, pre_confirmed=True, pre_approved=True) + workflow = self._mlist.subscription_policy( + self._mlist, anne, + pre_verified=True, pre_confirmed=True) list(workflow) # Anne was subscribed. self.assertIsNone(workflow.token) @@ -680,17 +697,18 @@ approval: def test_restore_user_absorbed(self): # The subscribing user is absorbed (and thus deleted) before the # moderator approves the subscription. - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_user(self._anne) bill = self._user_manager.create_user('bill@example.com') set_preferred(bill) # anne subscribes. - workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) + workflow = self._mlist.subscription_policy(self._mlist, anne, + pre_verified=True) list(workflow) # bill absorbs anne. bill.absorb(anne) # anne's subscription request is approved. - approved_workflow = SubscriptionWorkflow(self._mlist) + approved_workflow = self._mlist.subscription_policy(self._mlist) approved_workflow.token = workflow.token approved_workflow.restore() self.assertEqual(approved_workflow.user, bill) @@ -700,19 +718,19 @@ approval: def test_restore_address_absorbed(self): # The subscribing user is absorbed (and thus deleted) before the # moderator approves the subscription. - self._mlist.subscription_policy = SubscriptionPolicy.moderate + self._mlist.subscription_policy = ModerationSubscriptionPolicy anne = self._user_manager.create_user(self._anne) anne_address = anne.addresses[0] bill = self._user_manager.create_user('bill@example.com') # anne subscribes. - workflow = SubscriptionWorkflow( - self._mlist, anne_address, pre_verified=True) + workflow = self._mlist.subscription_policy( + self._mlist, anne_address, pre_verified=True) list(workflow) # bill absorbs anne. bill.absorb(anne) self.assertIn(anne_address, bill.addresses) # anne's subscription request is approved. - approved_workflow = SubscriptionWorkflow(self._mlist) + approved_workflow = self._mlist.subscription_policy(self._mlist) approved_workflow.token = workflow.token approved_workflow.restore() self.assertEqual(approved_workflow.user, bill) diff --git a/src/mailman/app/tests/test_unsubscriptions.py b/src/mailman/workflows/tests/test_unsubscriptions.py index 4ca657190..2e210e90b 100644 --- a/src/mailman/app/tests/test_unsubscriptions.py +++ b/src/mailman/workflows/tests/test_unsubscriptions.py @@ -21,14 +21,15 @@ import unittest from contextlib import suppress from mailman.app.lifecycle import create_list -from mailman.app.subscriptions import UnSubscriptionWorkflow -from mailman.interfaces.mailinglist import SubscriptionPolicy from mailman.interfaces.pending import IPendings from mailman.interfaces.subscriptions import TokenOwner from mailman.interfaces.usermanager import IUserManager from mailman.testing.helpers import LogFileMark, get_queue_messages from mailman.testing.layers import ConfigLayer from mailman.utilities.datetime import now +from mailman.workflows.unsubscription import ( + ConfirmModerationUnsubscriptionPolicy, ConfirmUnsubscriptionPolicy, + ModerationUnsubscriptionPolicy, OpenUnsubscriptionPolicy) from unittest.mock import patch from zope.component import getUtility @@ -40,7 +41,7 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): def setUp(self): self._mlist = create_list('test@example.com') self._mlist.admin_immed_notify = False - self._mlist.unsubscription_policy = SubscriptionPolicy.open + self._mlist.unsubscription_policy = OpenUnsubscriptionPolicy self._mlist.send_welcome_message = False self._anne = 'anne@example.com' self._user_manager = getUtility(IUserManager) @@ -58,7 +59,7 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): def test_start_state(self): # Test the workflow starts with no tokens or members. - workflow = UnSubscriptionWorkflow(self._mlist) + workflow = self._mlist.unsubscription_policy(self._mlist) self.assertEqual(workflow.token_owner, TokenOwner.no_one) self.assertIsNone(workflow.token) self.assertIsNone(workflow.member) @@ -67,8 +68,8 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # Test there is a Pendable object associated with a held # unsubscription request and it has some valid data associated with # it. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) with suppress(StopIteration): workflow.run_thru('send_confirmation') self.assertIsNotNone(workflow.token) @@ -84,23 +85,23 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): def test_user_or_address_required(self): # The `subscriber` attribute must be a user or address that is provided # to the workflow. - workflow = UnSubscriptionWorkflow(self._mlist) + workflow = OpenUnsubscriptionPolicy(self._mlist) self.assertRaises(AssertionError, list, workflow) def test_user_is_subscribed_to_unsubscribe(self): # A user must be subscribed to a list when trying to unsubscribe. addr = self._user_manager.create_address('aperson@example.org') addr.verfied_on = now() - workflow = UnSubscriptionWorkflow(self._mlist, addr) + workflow = self._mlist.unsubscription_policy(self._mlist, addr) self.assertRaises(AssertionError, workflow.run_thru, 'subscription_checks') def test_confirmation_checks_open_list(self): # An unsubscription from an open list does not need to be confirmed or # moderated. - self._mlist.unsubscription_policy = SubscriptionPolicy.open - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) - workflow.run_thru('confirmation_checks') + self._mlist.unsubscription_policy = OpenUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) + workflow.run_thru('subscription_checks') with patch.object(workflow, '_step_do_unsubscription') as step: next(workflow) step.assert_called_once_with() @@ -108,10 +109,9 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): def test_confirmation_checks_no_user_confirmation_needed(self): # An unsubscription from a list which does not need user confirmation # skips to the moderation checks. - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow(self._mlist, self.anne, - pre_confirmed=True) - workflow.run_thru('confirmation_checks') + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) + workflow.run_thru('subscription_checks') with patch.object(workflow, '_step_moderation_checks') as step: next(workflow) step.assert_called_once_with() @@ -120,8 +120,8 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # The unsubscription policy requires user-confirmation, but their # unsubscription is pre-confirmed. Since moderation is not reuqired, # the user will be immediately unsubscribed. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow( + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy( self._mlist, self.anne, pre_confirmed=True) workflow.run_thru('confirmation_checks') with patch.object(workflow, '_step_do_unsubscription') as step: @@ -133,19 +133,19 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # unsubscription is pre-confirmed. Since moderation is required, that # check will be performed. self._mlist.unsubscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) - workflow = UnSubscriptionWorkflow( + ConfirmModerationUnsubscriptionPolicy) + workflow = self._mlist.unsubscription_policy( self._mlist, self.anne, pre_confirmed=True) workflow.run_thru('confirmation_checks') - with patch.object(workflow, '_step_do_unsubscription') as step: + with patch.object(workflow, '_step_moderation_checks') as step: next(workflow) step.assert_called_once_with() def test_send_confirmation_checks_confirm_list(self): # The unsubscription policy requires user confirmation and the # unsubscription is not pre-confirmed. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) workflow.run_thru('confirmation_checks') with patch.object(workflow, '_step_send_confirmation') as step: next(workflow) @@ -153,17 +153,17 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): def test_moderation_checks_moderated_list(self): # The unsubscription policy requires moderation. - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) - workflow.run_thru('confirmation_checks') + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) + workflow.run_thru('subscription_checks') with patch.object(workflow, '_step_moderation_checks') as step: next(workflow) step.assert_called_once_with() def test_moderation_checks_approval_required(self): # The moderator must approve the subscription request. - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) workflow.run_thru('moderation_checks') with patch.object(workflow, '_step_get_moderator_approval') as step: next(workflow) @@ -172,8 +172,8 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): def test_do_unsusbcription(self): # An open unsubscription policy means the user gets unsubscribed to # the mailing list without any further confirmations or approvals. - self._mlist.unsubscription_policy = SubscriptionPolicy.open - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + self._mlist.unsubscription_policy = OpenUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) list(workflow) member = self._mlist.regular_members.get_member(self._anne) self.assertIsNone(member) @@ -182,9 +182,9 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # A moderation-requiring subscription policy plus a pre-approved # address means the user gets unsubscribed from the mailing list # without any further confirmation or approvals. - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow(self._mlist, self.anne, - pre_approved=True) + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne, + pre_approved=True) list(workflow) # Anne is now unsubscribed form the mailing list. member = self._mlist.regular_members.get_member(self._anne) @@ -198,10 +198,10 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # address means the user gets unsubscribed to the mailing list without # any further confirmations or approvals. self._mlist.unsubscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) - workflow = UnSubscriptionWorkflow(self._mlist, self.anne, - pre_approved=True, - pre_confirmed=True) + ConfirmModerationUnsubscriptionPolicy) + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne, + pre_approved=True, + pre_confirmed=True) list(workflow) member = self._mlist.regular_members.get_member(self._anne) self.assertIsNone(member) @@ -212,10 +212,8 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): def test_do_unsubscription_cleanups(self): # Once the user is unsubscribed, the token and its associated pending # database record will be removed from the database. - self._mlist.unsubscription_policy = SubscriptionPolicy.open - workflow = UnSubscriptionWorkflow(self._mlist, self.anne, - pre_approved=True, - pre_confirmed=True) + self._mlist.unsubscription_policy = OpenUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) # Run the workflow. list(workflow) # Anne is now unsubscribed from the list. @@ -229,9 +227,9 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # The workflow runs until moderator approval is required, at which # point the workflow is saved. Once the moderator approves, the # workflow resumes and the user is unsubscribed. - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow( - self._mlist, self.anne, pre_confirmed=True) + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy( + self._mlist, self.anne) # Run the entire workflow. list(workflow) # The user is currently subscribed to the mailing list. @@ -244,7 +242,7 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # Create a new workflow with the previous workflow's save token, and # restore its state. This models an approved un-sunscription request # and should result in the user getting subscribed. - approved_workflow = UnSubscriptionWorkflow(self._mlist) + approved_workflow = self._mlist.unsubscription_policy(self._mlist) approved_workflow.token = workflow.token approved_workflow.restore() list(approved_workflow) @@ -260,9 +258,9 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # When the unsubscription is held for moderator approval, a message is # logged. mark = LogFileMark('mailman.subscribe') - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow( - self._mlist, self.anne, pre_confirmed=True) + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy( + self._mlist, self.anne) # Run the entire workflow. list(workflow) self.assertIn( @@ -277,9 +275,9 @@ class TestUnSubscriptionWorkflow(unittest.TestCase): # When the unsubscription is held for moderator approval, and the list # is so configured, a notification is sent to the list moderators. self._mlist.admin_immed_notify = True - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow( - self._mlist, self.anne, pre_confirmed=True) + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy( + self._mlist, self.anne) # Consume the entire state machine. list(workflow) items = get_queue_messages('virgin', expected_count=1) @@ -305,9 +303,9 @@ request approval: # the list is so configured, a notification is sent to the list # moderators. self._mlist.admin_immed_notify = False - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow( - self._mlist, self.anne, pre_confirmed=True) + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy( + self._mlist, self.anne) # Consume the entire state machine. list(workflow) get_queue_messages('virgin', expected_count=0) @@ -318,9 +316,9 @@ request approval: def test_send_confirmation(self): # A confirmation message gets sent when the unsubscription must be # confirmed. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy # Run the workflow to model the confirmation step. - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) list(workflow) items = get_queue_messages('virgin', expected_count=1) message = items[0].msg @@ -336,8 +334,8 @@ request approval: def test_do_confirmation_unsubscribes_user(self): # Unsubscriptions to the mailing list must be confirmed. Once that's # done, the user's address is unsubscribed. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) list(workflow) # Anne is a member. member = self._mlist.regular_members.get_member(self._anne) @@ -347,7 +345,7 @@ request approval: self.assertIsNotNone(workflow.token) self.assertEqual(workflow.token_owner, TokenOwner.subscriber) # Confirm. - confirm_workflow = UnSubscriptionWorkflow(self._mlist) + confirm_workflow = self._mlist.unsubscription_policy(self._mlist) confirm_workflow.token = workflow.token confirm_workflow.restore() list(confirm_workflow) @@ -363,8 +361,8 @@ request approval: # done, the address is unsubscribed. address = self.anne.register('anne.person@example.com') self._mlist.subscribe(address) - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow(self._mlist, address) + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, address) list(workflow) # Bart is a member. member = self._mlist.regular_members.get_member( @@ -375,7 +373,7 @@ request approval: self.assertIsNotNone(workflow.token) self.assertEqual(workflow.token_owner, TokenOwner.subscriber) # Confirm. - confirm_workflow = UnSubscriptionWorkflow(self._mlist) + confirm_workflow = self._mlist.unsubscription_policy(self._mlist) confirm_workflow.token = workflow.token confirm_workflow.restore() list(confirm_workflow) @@ -390,8 +388,8 @@ request approval: def test_do_confirmation_nonmember(self): # Attempt to confirm the unsubscription of a member who has already # been unsubscribed. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) list(workflow) # Anne is a member. member = self._mlist.regular_members.get_member(self._anne) @@ -403,7 +401,7 @@ request approval: # Unsubscribe Anne out of band. member.unsubscribe() # Confirm. - confirm_workflow = UnSubscriptionWorkflow(self._mlist) + confirm_workflow = self._mlist.unsubscription_policy(self._mlist) confirm_workflow.token = workflow.token confirm_workflow.restore() list(confirm_workflow) @@ -414,8 +412,8 @@ request approval: def test_do_confirmation_nonmember_final_step(self): # Attempt to confirm the unsubscription of a member who has already # been unsubscribed. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) list(workflow) # Anne is a member. member = self._mlist.regular_members.get_member(self._anne) @@ -425,7 +423,7 @@ request approval: self.assertIsNotNone(workflow.token) self.assertEqual(workflow.token_owner, TokenOwner.subscriber) # Confirm. - confirm_workflow = UnSubscriptionWorkflow(self._mlist) + confirm_workflow = self._mlist.unsubscription_policy(self._mlist) confirm_workflow.token = workflow.token confirm_workflow.restore() confirm_workflow.run_until('do_unsubscription') @@ -443,8 +441,8 @@ request approval: # the user confirming their subscription, and then the moderator # approving it, that different tokens are used in these two cases. self._mlist.unsubscription_policy = ( - SubscriptionPolicy.confirm_then_moderate) - workflow = UnSubscriptionWorkflow(self._mlist, self.anne) + ConfirmModerationUnsubscriptionPolicy) + workflow = self._mlist.unsubscription_policy(self._mlist, self.anne) # Run the state machine up to the first confirmation, and cache the # confirmation token. list(workflow) @@ -457,7 +455,7 @@ request approval: self.assertIsNotNone(workflow.token) self.assertEqual(workflow.token_owner, TokenOwner.subscriber) # The old token will not work for moderator approval. - moderator_workflow = UnSubscriptionWorkflow(self._mlist) + moderator_workflow = self._mlist.unsubscription_policy(self._mlist) moderator_workflow.token = token moderator_workflow.restore() list(moderator_workflow) @@ -468,7 +466,7 @@ request approval: # that there's a new token for the next steps. self.assertNotEqual(token, moderator_workflow.token) # The old token won't work. - final_workflow = UnSubscriptionWorkflow(self._mlist) + final_workflow = self._mlist.unsubscription_policy(self._mlist) final_workflow.token = token self.assertRaises(LookupError, final_workflow.restore) # Running this workflow will fail. @@ -492,9 +490,9 @@ request approval: def test_confirmation_needed_and_pre_confirmed(self): # The subscription policy is 'confirm' but the subscription is # pre-confirmed so the moderation checks can be skipped. - self._mlist.unsubscription_policy = SubscriptionPolicy.confirm - workflow = UnSubscriptionWorkflow( - self._mlist, self.anne, pre_confirmed=True, pre_approved=True) + self._mlist.unsubscription_policy = ConfirmUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy( + self._mlist, self.anne, pre_confirmed=True) list(workflow) # Anne was unsubscribed. self.assertIsNone(workflow.token) @@ -504,11 +502,11 @@ request approval: def test_confirmation_needed_moderator_address(self): address = self.anne.register('anne.person@example.com') self._mlist.subscribe(address) - self._mlist.unsubscription_policy = SubscriptionPolicy.moderate - workflow = UnSubscriptionWorkflow(self._mlist, address) + self._mlist.unsubscription_policy = ModerationUnsubscriptionPolicy + workflow = self._mlist.unsubscription_policy(self._mlist, address) # Get moderator approval. list(workflow) - approved_workflow = UnSubscriptionWorkflow(self._mlist) + approved_workflow = self._mlist.unsubscription_policy(self._mlist) approved_workflow.token = workflow.token approved_workflow.restore() list(approved_workflow) diff --git a/src/mailman/app/tests/test_workflow.py b/src/mailman/workflows/tests/test_workflow.py index e82001540..3e7856b29 100644 --- a/src/mailman/app/tests/test_workflow.py +++ b/src/mailman/workflows/tests/test_workflow.py @@ -20,15 +20,15 @@ import json import unittest -from mailman.app.workflow import Workflow -from mailman.interfaces.workflow import IWorkflowStateManager +from mailman.interfaces.workflows import IWorkflowStateManager from mailman.testing.layers import ConfigLayer +from mailman.workflows.base import Workflow from zope.component import getUtility class MyWorkflow(Workflow): - INITIAL_STATE = 'first' - SAVE_ATTRIBUTES = ('ant', 'bee', 'cat') + initial_state = 'first' + save_attributes = ('ant', 'bee', 'cat') def __init__(self): super().__init__() @@ -51,7 +51,7 @@ class MyWorkflow(Workflow): class DependentWorkflow(MyWorkflow): - SAVE_ATTRIBUTES = ('ant', 'bee', 'cat', 'elf') + save_attributes = ('ant', 'bee', 'cat', 'elf') def __init__(self): super().__init__() @@ -136,7 +136,7 @@ class TestWorkflow(unittest.TestCase): def test_save_and_restore_dependant_attributes(self): # Attributes must be restored in the order they are declared in - # SAVE_ATTRIBUTES. + # save_attributes. workflow = iter(DependentWorkflow()) workflow.elf = 6 workflow.save() @@ -153,7 +153,7 @@ class TestWorkflow(unittest.TestCase): # Save the state of an old version of the workflow that would not have # the cat attribute. state_manager.save( - self._workflow.token, 'first', + self._workflow.token, '["first"]', json.dumps({'ant': 1, 'bee': 2})) # Restore in the current version that needs the cat attribute. new_workflow = MyWorkflow() diff --git a/src/mailman/workflows/unsubscription.py b/src/mailman/workflows/unsubscription.py new file mode 100644 index 000000000..45dad92f5 --- /dev/null +++ b/src/mailman/workflows/unsubscription.py @@ -0,0 +1,190 @@ +# Copyright (C) 2015-2017 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/>. + +"""""" + +from mailman.core.i18n import _ +from mailman.interfaces.workflows import IUnsubscriptionWorkflow +from mailman.workflows.common import (ConfirmationMixin, ModerationMixin, + UnsubscriptionBase) +from public import public +from zope.interface import implementer + + +@public +@implementer(IUnsubscriptionWorkflow) +class OpenUnsubscriptionPolicy(UnsubscriptionBase): + """""" + + name = 'unsub-policy-open' + description = _( + 'An open unsubscription policy, only requires verification.') + initial_state = 'prepare' + save_attributes = ( + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None): + """ + + :param mlist: + :param subscriber: The user or address to unsubscribe. + :type subscriber: ``IUser`` or ``IAddress`` + """ + UnsubscriptionBase.__init__(self, mlist, subscriber) + + def _step_prepare(self): + self.push('do_unsubscription') + self.push('subscription_checks') + + +@public +@implementer(IUnsubscriptionWorkflow) +class ConfirmUnsubscriptionPolicy(UnsubscriptionBase, ConfirmationMixin): + """""" + + name = 'unsub-policy-confirm' + description = _('An unsubscription policy that requires confirmation.') + initial_state = 'prepare' + save_attributes = ( + 'confirmed', + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None, *, + pre_confirmed=False): + """ + + :param mlist: + :param subscriber: The user or address to unsubscribe. + :type subscriber: ``IUser`` or ``IAddress`` + :param pre_confirmed: A flag indicating whether, when required by the + unsubscription policy, an unsubscription request should be + considered pre-confirmed. Normally in such cases, a mail-back + confirmation message is sent to the subscriber, which must be + positively acknowledged by some manner. Setting this flag to True + automatically confirms the unsubscription request. (A confirmation + message may still be sent under other conditions.) + :type pre_confirmed: bool + """ + UnsubscriptionBase.__init__(self, mlist, subscriber) + ConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) + + def _step_prepare(self): + self.push('do_unsubscription') + self.push('confirmation_checks') + self.push('subscription_checks') + + +@public +@implementer(IUnsubscriptionWorkflow) +class ModerationUnsubscriptionPolicy(UnsubscriptionBase, ModerationMixin): + """""" + + name = 'unsub-policy-moderate' + description = _('An unsubscription policy that requires moderation.') + initial_state = 'prepare' + save_attributes = ( + 'approved', + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None, *, + pre_approved=False): + """ + + :param mlist: + :param subscriber: The user or address to unsubscribe. + :type subscriber: ``IUser`` or ``IAddress`` + :param pre_approved: A flag indicating whether, when required by the + unsubscription policy, an unsubscription request should be + considered pre-approved. Normally in such cases, the list + administrator is notified that an approval is necessary, which + must be positively acknowledged in some manner. Setting this flag + to True automatically approves the unsubscription request. + :type pre_approved: bool + """ + UnsubscriptionBase.__init__(self, mlist, subscriber) + ModerationMixin.__init__(self, pre_approved=pre_approved) + + def _step_prepare(self): + self.push('do_unsubscription') + self.push('moderation_checks') + self.push('subscription_checks') + + +@public +@implementer(IUnsubscriptionWorkflow) +class ConfirmModerationUnsubscriptionPolicy(UnsubscriptionBase, + ConfirmationMixin, + ModerationMixin): + """""" + + name = 'unsub-policy-confirm-moderate' + description = _( + 'An unsubscription policy, requires moderation after confirmation.') + initial_state = 'prepare' + save_attributes = ( + 'approved', + 'confirmed', + 'address_key', + 'subscriber_key', + 'user_key', + 'token_owner_key', + ) + + def __init__(self, mlist, subscriber=None, *, + pre_confirmed=False, pre_approved=False): + """ + + :param mlist: + :param subscriber: The user or address to unsubscribe. + :type subscriber: ``IUser`` or ``IAddress`` + :param pre_confirmed: A flag indicating whether, when required by the + unsubscription policy, an unsubscription request should be + considered pre-confirmed. Normally in such cases, a mail-back + confirmation message is sent to the subscriber, which must be + positively acknowledged by some manner. Setting this flag to True + automatically confirms the unsubscription request. (A confirmation + message may still be sent under other conditions.) + :type pre_confirmed: bool + :param pre_approved: A flag indicating whether, when required by the + unsubscription policy, an unsubscription request should be + considered pre-approved. Normally in such cases, the list + administrator is notified that an approval is necessary, which + must be positively acknowledged in some manner. Setting this flag + to True automatically approves the unsubscription request. + :type pre_approved: bool + """ + UnsubscriptionBase.__init__(self, mlist, subscriber) + ConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) + ModerationMixin.__init__(self, pre_approved=pre_approved) + + def _step_prepare(self): + self.push('do_unsubscription') + self.push('moderation_checks') + self.push('confirmation_checks') + self.push('subscription_checks') |
