summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/workflows/common.py192
-rw-r--r--src/mailman/workflows/subscription.py231
2 files changed, 422 insertions, 1 deletions
diff --git a/src/mailman/workflows/common.py b/src/mailman/workflows/common.py
index c57ce374d..79d57d0ae 100644
--- a/src/mailman/workflows/common.py
+++ b/src/mailman/workflows/common.py
@@ -18,23 +18,37 @@
"""Common support between subscription and unsubscription."""
import uuid
+import logging
from datetime import timedelta
+from email.utils import formataddr
from enum import Enum
+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)
from mailman.interfaces.pending import IPendable, IPendings
-from mailman.interfaces.subscriptions import TokenOwner
+from mailman.interfaces.subscriptions import (
+ SubscriptionConfirmationNeededEvent, 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,
IUnsubscriptionWorkflow, IWorkflow)
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.event import notify
from zope.interface import implementer
from zope.interface.exceptions import DoesNotImplement
+log = logging.getLogger('mailman.subscribe')
+
+
class WhichSubscriber(Enum):
address = 1
user = 2
@@ -141,3 +155,179 @@ class SubscriptionWorkflowCommon(Workflow):
token_owner=token_owner.name,
)
self.token = pendings.add(pendable, timedelta(days=3650))
+
+
+class SubscriptionBase(SubscriptionWorkflowCommon):
+
+ 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: no cover
+ 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.
+ pendings = getUtility(IPendings).find(
+ mlist=self.mlist,
+ pend_type='subscription')
+ 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 RequestMixin:
+
+ def _step_send_confirmation(self):
+ self._set_token(TokenOwner.subscriber)
+ self.push('do_confirm_verify')
+ self.save()
+ # Triggering this event causes the confirmation message to be sent.
+ notify(SubscriptionConfirmationNeededEvent(
+ 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
+ # 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
+
+
+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')
+ return
+
+
+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 can be skipped.
+ if self.confirmed:
+ return
+ # 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 subscription request?
+ if self.approved:
+ return
+ # A moderator must confirm the subscription.
+ 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.
+ 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 <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_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
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')