summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJ08nY2017-07-04 19:45:11 +0200
committerJ08nY2017-08-07 18:18:00 +0200
commitbf8550eaa2b65dd6c546eaf153dfefc8d2f4fe5f (patch)
treeac674a3070ca5d41aab4a2ebd688032bb1e23643
parent505f47025ca3fab68a489238837c1a84e4b2a1b6 (diff)
downloadmailman-bf8550eaa2b65dd6c546eaf153dfefc8d2f4fe5f.tar.gz
mailman-bf8550eaa2b65dd6c546eaf153dfefc8d2f4fe5f.tar.zst
mailman-bf8550eaa2b65dd6c546eaf153dfefc8d2f4fe5f.zip
-rw-r--r--src/mailman/workflows/common.py121
-rw-r--r--src/mailman/workflows/unsubscription.py190
2 files changed, 309 insertions, 2 deletions
diff --git a/src/mailman/workflows/common.py b/src/mailman/workflows/common.py
index 79d57d0ae..d6c372b1e 100644
--- a/src/mailman/workflows/common.py
+++ b/src/mailman/workflows/common.py
@@ -23,15 +23,19 @@ import logging
from datetime import timedelta
from email.utils import formataddr
from enum import Enum
+
+from mailman.app.membership import delete_member
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)
+ MembershipIsBannedError,
+ NotAMemberError)
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.subscriptions import (
- SubscriptionConfirmationNeededEvent, SubscriptionPendingError, TokenOwner)
+ SubscriptionConfirmationNeededEvent, SubscriptionPendingError, TokenOwner,
+ UnsubscriptionConfirmationNeededEvent)
from mailman.interfaces.template import ITemplateLoader
from mailman.interfaces.user import IUser
from mailman.interfaces.usermanager import IUserManager
@@ -331,3 +335,116 @@ class ModerationMixin:
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.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
+ 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')
+
+
+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))
+ if self.mlist.admin_immed_notify:
+ subject = _(
+ 'New unsubscription 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)
+ 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_unsubscribe_from_restored(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
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 <http://www.gnu.org/licenses/>.
+
+""""""
+
+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')