From bf8550eaa2b65dd6c546eaf153dfefc8d2f4fe5f Mon Sep 17 00:00:00 2001 From: J08nY Date: Tue, 4 Jul 2017 19:45:11 +0200 Subject: Split unsubscription workflow into mixins. --- src/mailman/workflows/unsubscription.py | 190 ++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/mailman/workflows/unsubscription.py (limited to 'src/mailman/workflows/unsubscription.py') diff --git a/src/mailman/workflows/unsubscription.py b/src/mailman/workflows/unsubscription.py new file mode 100644 index 000000000..3c4ad39de --- /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 . + +"""""" + +from mailman.core.i18n import _ +from mailman.interfaces.workflows import IUnsubscriptionWorkflow +from mailman.workflows.common import (UnConfirmationMixin, UnModerationMixin, + 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, UnConfirmationMixin): + """""" + + 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) + UnConfirmationMixin.__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, UnModerationMixin): + """""" + + 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) + UnModerationMixin.__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, + UnConfirmationMixin, + UnModerationMixin): + """""" + + 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) + UnConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) + UnModerationMixin.__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') -- cgit v1.2.3-70-g09d2 From 7e47b06fbd20ac349b653f4cfcd2157229be1176 Mon Sep 17 00:00:00 2001 From: J08nY Date: Thu, 6 Jul 2017 18:13:47 +0200 Subject: Refactor the duplicate workflow mixins. --- src/mailman/workflows/common.py | 228 +++++++++++--------------------- src/mailman/workflows/unsubscription.py | 18 +-- 2 files changed, 88 insertions(+), 158 deletions(-) (limited to 'src/mailman/workflows/unsubscription.py') diff --git a/src/mailman/workflows/common.py b/src/mailman/workflows/common.py index d6c372b1e..8593d0997 100644 --- a/src/mailman/workflows/common.py +++ b/src/mailman/workflows/common.py @@ -58,6 +58,11 @@ class WhichSubscriber(Enum): user = 2 +class WhichWorkflow(Enum): + subscription = 1 + unsubscription = 2 + + @implementer(IPendable) class PendableSubscription(dict): PEND_TYPE = 'subscription' @@ -130,6 +135,16 @@ class SubscriptionWorkflowCommon(Workflow): 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) @@ -163,6 +178,10 @@ class SubscriptionWorkflowCommon(Workflow): 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 @@ -217,27 +236,49 @@ class SubscriptionBase(SubscriptionWorkflowCommon): '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: + event_class = SubscriptionConfirmationNeededEvent + else: + event_class = UnsubscriptionConfirmationNeededEvent + # Triggering this event causes the confirmation message to be sent. - notify(SubscriptionConfirmationNeededEvent( - self.mlist, self.token, self.address.email)) + notify(event_class( + self.mlist, self.token, self.address.email)) # 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 - # (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 + # 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 @@ -249,6 +290,10 @@ class RequestMixin: 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): @@ -265,7 +310,6 @@ class VerificationMixin(RequestMixin): # to send a validation email that will also confirm that the # user wants to be subscribed to this mailing list. self.push('send_confirmation') - return class ConfirmationMixin(RequestMixin): @@ -275,11 +319,10 @@ class ConfirmationMixin(RequestMixin): def _step_confirmation_checks(self): # If the subscription has been pre-confirmed, then we can skip the - # confirmation check can be skipped. - if self.confirmed: - return - # The user must confirm their subscription. - self.push('send_confirmation') + # confirmation check. + if not self.confirmed: + # The user must confirm their subscription. + self.push('send_confirmation') class ModerationMixin: @@ -288,146 +331,36 @@ class ModerationMixin: self.approved = pre_approved def _step_moderation_checks(self): - # Does the moderator need to approve the subscription request? - if self.approved: - return - # A moderator must confirm the subscription. - self.push('get_moderator_approval') + # 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 subscription. If they don't, the workflow and - # subscription request will just be thrown away. + # approves of the request. If they don't, the workflow and + # request will just be thrown away. self._set_token(TokenOwner.moderator) - self.push('subscribe_from_restored') - self.save() - log.info('{}: held subscription request from {}'.format( - self.mlist.fqdn_listname, self.address.email)) - # Possibly send a notification to the list moderators. - if self.mlist.admin_immed_notify: - subject = _( - 'New subscription request to $self.mlist.display_name ' - 'from $self.address.email') - username = formataddr( - (self.subscriber.display_name, self.address.email)) - template = getUtility(ITemplateLoader).get( - 'list:admin:action:subscribe', self.mlist) - text = wrap(expand(template, self.mlist, dict( - member=username, - ))) - # This message should appear to come from the -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_subscribe_from_restored(self): - # Prevent replay attacks. - self._set_token(TokenOwner.no_one) - # Restore a little extra state that can't be stored in the database - # (because the order of setattr() on restore is indeterminate), then - # subscribe the user. - if self.which is WhichSubscriber.address: - self.subscriber = self.address - else: - assert self.which is WhichSubscriber.user - self.subscriber = self.user - - -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) - - 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 UnRequestMixin: - - def _step_send_confirmation(self): - self._set_token(TokenOwner.subscriber) - self.push('do_confirm_verify') + self.push('restore') self.save() - notify(UnsubscriptionConfirmationNeededEvent( - self.mlist, self.token, self.address.email)) - raise StopIteration - def _step_do_confirm_verify(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 + if self._workflow is WhichWorkflow.subscription: + workflow_name = 'subscription' + template_name = 'list:admin:action:subscribe' else: - assert self.which is WhichSubscriber.user - self.subscriber = self.user - # Reset the token so it can't be used in a replay attack. - self._set_token(TokenOwner.no_one) - # Restore the member object. - self.member = self.mlist.regular_members.get_member(self.address.email) - # It's possible the member was already unsubscribed while we were - # waiting for the confirmation. - if self.member is None: - return - # The user has confirmed their unsubscription request - self.confirmed = True - - -class UnConfirmationMixin(UnRequestMixin): - - def __init__(self, pre_confirmed=False): - self.confirmed = pre_confirmed - - def _step_confirmation_checks(self): - # If the unsubscription has been pre-confirmed, then we can skip the - # confirmation check can be skipped. - if self.confirmed: - return - # The user must confirm their unsubscription. - self.push('send_confirmation') - + workflow_name = 'unsubscription' + template_name = 'list:admin:action:unsubscribe' -class UnModerationMixin(UnRequestMixin): - - def __init__(self, pre_approved=False): - self.approved = pre_approved - - def _step_moderation_checks(self): - # Does the moderator need to approve the unsubscription request? - if not self.approved: - self.push('get_moderator_approval') - - def _step_get_moderator_approval(self): - self._set_token(TokenOwner.moderator) - self.push('unsubscribe_from_restored') - self.save() - log.info('{}: held unsubscription request from {}'.format( - self.mlist.fqdn_listname, self.address.email)) + 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 unsubscription request to $self.mlist.display_name ' + '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( - 'list:admin:action:unsubscribe', self.mlist) + template_name, self.mlist) text = wrap(expand(template, self.mlist, dict( member=username, ))) @@ -437,14 +370,11 @@ class UnModerationMixin(UnRequestMixin): self.mlist.owner_address, self.mlist.owner_address, subject, text, self.mlist.preferred_language) msg.send(self.mlist) - # The workflow must stop running here + # The workflow must stop running here. raise StopIteration - def _step_unsubscribe_from_restored(self): + def _step_restore(self): # Prevent replay attacks. self._set_token(TokenOwner.no_one) - if self.which is WhichSubscriber.address: - self.subscriber = self.address - else: - assert self.which is WhichSubscriber.user - self.subscriber = self.user + # Restore a little extra state that can't be stored in the database. + self._restore_subscriber() diff --git a/src/mailman/workflows/unsubscription.py b/src/mailman/workflows/unsubscription.py index 3c4ad39de..45dad92f5 100644 --- a/src/mailman/workflows/unsubscription.py +++ b/src/mailman/workflows/unsubscription.py @@ -19,7 +19,7 @@ from mailman.core.i18n import _ from mailman.interfaces.workflows import IUnsubscriptionWorkflow -from mailman.workflows.common import (UnConfirmationMixin, UnModerationMixin, +from mailman.workflows.common import (ConfirmationMixin, ModerationMixin, UnsubscriptionBase) from public import public from zope.interface import implementer @@ -57,7 +57,7 @@ class OpenUnsubscriptionPolicy(UnsubscriptionBase): @public @implementer(IUnsubscriptionWorkflow) -class ConfirmUnsubscriptionPolicy(UnsubscriptionBase, UnConfirmationMixin): +class ConfirmUnsubscriptionPolicy(UnsubscriptionBase, ConfirmationMixin): """""" name = 'unsub-policy-confirm' @@ -88,7 +88,7 @@ class ConfirmUnsubscriptionPolicy(UnsubscriptionBase, UnConfirmationMixin): :type pre_confirmed: bool """ UnsubscriptionBase.__init__(self, mlist, subscriber) - UnConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) + ConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) def _step_prepare(self): self.push('do_unsubscription') @@ -98,7 +98,7 @@ class ConfirmUnsubscriptionPolicy(UnsubscriptionBase, UnConfirmationMixin): @public @implementer(IUnsubscriptionWorkflow) -class ModerationUnsubscriptionPolicy(UnsubscriptionBase, UnModerationMixin): +class ModerationUnsubscriptionPolicy(UnsubscriptionBase, ModerationMixin): """""" name = 'unsub-policy-moderate' @@ -128,7 +128,7 @@ class ModerationUnsubscriptionPolicy(UnsubscriptionBase, UnModerationMixin): :type pre_approved: bool """ UnsubscriptionBase.__init__(self, mlist, subscriber) - UnModerationMixin.__init__(self, pre_approved=pre_approved) + ModerationMixin.__init__(self, pre_approved=pre_approved) def _step_prepare(self): self.push('do_unsubscription') @@ -139,8 +139,8 @@ class ModerationUnsubscriptionPolicy(UnsubscriptionBase, UnModerationMixin): @public @implementer(IUnsubscriptionWorkflow) class ConfirmModerationUnsubscriptionPolicy(UnsubscriptionBase, - UnConfirmationMixin, - UnModerationMixin): + ConfirmationMixin, + ModerationMixin): """""" name = 'unsub-policy-confirm-moderate' @@ -180,8 +180,8 @@ class ConfirmModerationUnsubscriptionPolicy(UnsubscriptionBase, :type pre_approved: bool """ UnsubscriptionBase.__init__(self, mlist, subscriber) - UnConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) - UnModerationMixin.__init__(self, pre_approved=pre_approved) + ConfirmationMixin.__init__(self, pre_confirmed=pre_confirmed) + ModerationMixin.__init__(self, pre_approved=pre_approved) def _step_prepare(self): self.push('do_unsubscription') -- cgit v1.2.3-70-g09d2