# Copyright (C) 2009-2015 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 . """Handle subscriptions.""" __all__ = [ 'SubscriptionService', 'SubscriptionWorkflow', 'handle_ListDeletingEvent', ] import uuid import logging from email.utils import formataddr from enum import Enum from datetime import timedelta from mailman.app.membership import add_member, delete_member from mailman.app.workflow import Workflow from mailman.core.constants import system_preferences from mailman.core.i18n import _ from mailman.database.transaction import dbconnection from mailman.email.message import UserNotification from mailman.interfaces.address import IAddress from mailman.interfaces.bans import IBanManager from mailman.interfaces.listmanager import ( IListManager, ListDeletingEvent, NoSuchListError) from mailman.interfaces.mailinglist import SubscriptionPolicy from mailman.interfaces.member import ( DeliveryMode, MemberRole, MembershipIsBannedError) from mailman.interfaces.pending import IPendable, IPendings from mailman.interfaces.subscriptions import ( ISubscriptionService, MissingUserError, RequestRecord) from mailman.interfaces.user import IUser from mailman.interfaces.usermanager import IUserManager from mailman.model.member import Member from mailman.utilities.datetime import now from mailman.utilities.i18n import make from operator import attrgetter from sqlalchemy import and_, or_ from uuid import UUID from zope.component import getUtility from zope.interface import implementer log = logging.getLogger('mailman.subscribe') def _membership_sort_key(member): """Sort function for find_members(). The members are sorted first by unique list id, then by subscribed email address, then by role. """ return (member.list_id, member.address.email, member.role.value) class WhichSubscriber(Enum): address = 1 user = 2 @implementer(IPendable) class Pendable(dict): pass class SubscriptionWorkflow(Workflow): """Workflow of a subscription request.""" INITIAL_STATE = 'sanity_checks' SAVE_ATTRIBUTES = ( 'pre_approved', 'pre_confirmed', 'pre_verified', 'address_key', 'subscriber_key', 'user_key', ) def __init__(self, mlist, subscriber=None, *, pre_verified=False, pre_confirmed=False, pre_approved=False): super().__init__() self.mlist = mlist self.address = None self.user = None self.which = None # 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 self.pre_verified = pre_verified self.pre_confirmed = pre_confirmed self.pre_approved = pre_approved @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) assert self.user is not None @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) 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 email address banned? if IBanManager(self.mlist).is_banned(self.address.email): raise MembershipIsBannedError(self.mlist, self.address.email) self.push('verification_checks') 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.pre_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 self.push('confirmation_checks') def _step_confirmation_checks(self): # If the list's subscription policy is open, then the user can be # subscribed right here and now. if self.mlist.subscription_policy is SubscriptionPolicy.open: self.push('do_subscription') return # If we do not need the user's confirmation, then skip to the # moderation checks. if self.mlist.subscription_policy is SubscriptionPolicy.moderate: self.push('moderation_checks') return # If the subscription has been pre-confirmed, then we can skip to the # moderation checks. if self.pre_confirmed: self.push('moderation_checks') return # The user must confirm their subscription. self.push('send_confirmation') def _step_moderation_checks(self): # Does the moderator need to approve the subscription request? assert self.mlist.subscription_policy in ( SubscriptionPolicy.moderate, SubscriptionPolicy.confirm_then_moderate) if self.pre_approved: self.push('do_subscription') else: self.push('get_moderator_approval') def _step_do_subscription(self): # We can immediately subscribe the user to the mailing list. self.mlist.subscribe(self.subscriber) def _step_get_moderator_approval(self): # Getting the moderator's approval requires several steps. We'll need # to suspend this workflow for an indeterminate amount of time while # we wait for that approval. We need a unique token for this # suspended workflow, so we'll create a minimal pending record. We # also might need to send an email notification to the list # moderators. # # Start by creating the pending record. This will give us a hash # token we can use to uniquely name this workflow. It only needs to # contain the current date, which we'll use to expire requests from # the database, say if the moderator never approves the request. pendable = Pendable(when=now().isoformat()) self.token = getUtility(IPendings).add(pendable, timedelta(days=3650)) # 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.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)) text = make('subauth.txt', mailing_list=self.mlist, username=username, listname=self.mlist.fqdn_listname, ) # 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, tomoderators=True) # The workflow must stop running here. raise StopIteration def _step_subscribe_from_restored(self): # 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 self.push('do_subscription') def _step_send_confirmation(self): self._next.append('moderation_check') self.save() self._next.clear() # stop iteration until we get confirmation # XXX: create the Pendable, send the ConfirmationNeededEvent # (see Registrar.register) @implementer(ISubscriptionService) class SubscriptionService: """Subscription services for the REST API.""" __name__ = 'members' def get_members(self): """See `ISubscriptionService`.""" # {list_id -> {role -> [members]}} by_list = {} user_manager = getUtility(IUserManager) for member in user_manager.members: by_role = by_list.setdefault(member.list_id, {}) members = by_role.setdefault(member.role.name, []) members.append(member) # Flatten into single list sorted as per the interface. all_members = [] address_of_member = attrgetter('address.email') for list_id in sorted(by_list): by_role = by_list[list_id] all_members.extend( sorted(by_role.get('owner', []), key=address_of_member)) all_members.extend( sorted(by_role.get('moderator', []), key=address_of_member)) all_members.extend( sorted(by_role.get('member', []), key=address_of_member)) return all_members @dbconnection def get_member(self, store, member_id): """See `ISubscriptionService`.""" members = store.query(Member).filter(Member._member_id == member_id) if members.count() == 0: return None else: assert members.count() == 1, 'Too many matching members' return members[0] @dbconnection def find_members(self, store, subscriber=None, list_id=None, role=None): """See `ISubscriptionService`.""" # If `subscriber` is a user id, then we'll search for all addresses # which are controlled by the user, otherwise we'll just search for # the given address. user_manager = getUtility(IUserManager) if subscriber is None and list_id is None and role is None: return [] # Querying for the subscriber is the most complicated part, because # the parameter can either be an email address or a user id. query = [] if subscriber is not None: if isinstance(subscriber, str): # subscriber is an email address. address = user_manager.get_address(subscriber) user = user_manager.get_user(subscriber) # This probably could be made more efficient. if address is None or user is None: return [] query.append(or_(Member.address_id == address.id, Member.user_id == user.id)) else: # subscriber is a user id. user = user_manager.get_user_by_id(subscriber) address_ids = list(address.id for address in user.addresses if address.id is not None) if len(address_ids) == 0 or user is None: return [] query.append(or_(Member.user_id == user.id, Member.address_id.in_(address_ids))) # Calculate the rest of the query expression, which will get And'd # with the Or clause above (if there is one). if list_id is not None: query.append(Member.list_id == list_id) if role is not None: query.append(Member.role == role) results = store.query(Member).filter(and_(*query)) return sorted(results, key=_membership_sort_key) def __iter__(self): for member in self.get_members(): yield member def join(self, list_id, subscriber, display_name=None, delivery_mode=DeliveryMode.regular, role=MemberRole.member): """See `ISubscriptionService`.""" mlist = getUtility(IListManager).get_by_list_id(list_id) if mlist is None: raise NoSuchListError(list_id) # Is the subscriber an email address or user id? if isinstance(subscriber, str): if display_name is None: display_name, at, domain = subscriber.partition('@') return add_member( mlist, RequestRecord(subscriber, display_name, delivery_mode, system_preferences.preferred_language), role) else: # We have to assume it's a UUID. assert isinstance(subscriber, UUID), 'Not a UUID' user = getUtility(IUserManager).get_user_by_id(subscriber) if user is None: raise MissingUserError(subscriber) return mlist.subscribe(user, role) def leave(self, list_id, email): """See `ISubscriptionService`.""" mlist = getUtility(IListManager).get_by_list_id(list_id) if mlist is None: raise NoSuchListError(list_id) # XXX for now, no notification or user acknowledgment. delete_member(mlist, email, False, False) def handle_ListDeletingEvent(event): """Delete a mailing list's members when the list is being deleted.""" if not isinstance(event, ListDeletingEvent): return # Find all the members still associated with the mailing list. members = getUtility(ISubscriptionService).find_members( list_id=event.mailing_list.list_id) for member in members: member.unsubscribe()