Subscription workflow checkpointing.
[mailman.git] / src / mailman / app / subscriptions.py
blob2deec131b56efa5aace263411b2c5ebb5b184aaa
1 # Copyright (C) 2009-2015 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Handle subscriptions."""
20 __all__ = [
21 'SubscriptionService',
22 'SubscriptionWorkflow',
23 'handle_ListDeletingEvent',
28 from mailman.app.membership import add_member, delete_member
29 from mailman.app.moderator import hold_subscription
30 from mailman.app.workflow import Workflow
31 from mailman.core.constants import system_preferences
32 from mailman.database.transaction import dbconnection
33 from mailman.interfaces.address import IAddress
34 from mailman.interfaces.listmanager import (
35 IListManager, ListDeletingEvent, NoSuchListError)
36 from mailman.interfaces.mailinglist import SubscriptionPolicy
37 from mailman.interfaces.member import DeliveryMode, MemberRole
38 from mailman.interfaces.subscriptions import (
39 ISubscriptionService, MissingUserError, RequestRecord)
40 from mailman.interfaces.user import IUser
41 from mailman.interfaces.usermanager import IUserManager
42 from mailman.model.member import Member
43 from mailman.utilities.datetime import now
44 from operator import attrgetter
45 from sqlalchemy import and_, or_
46 from uuid import UUID
47 from zope.component import getUtility
48 from zope.interface import implementer
52 def _membership_sort_key(member):
53 """Sort function for find_members().
55 The members are sorted first by unique list id, then by subscribed email
56 address, then by role.
57 """
58 return (member.list_id, member.address.email, member.role.value)
62 class SubscriptionWorkflow(Workflow):
63 """Workflow of a subscription request."""
65 INITIAL_STATE = 'sanity_checks'
66 SAVE_ATTRIBUTES = (
67 'pre_approved',
68 'pre_confirmed',
69 'pre_verified',
72 def __init__(self, mlist, subscriber, *,
73 pre_verified=False, pre_confirmed=False, pre_approved=False):
74 super().__init__()
75 self.mlist = mlist
76 # The subscriber must be either an IUser or IAddress.
77 if IAddress.providedBy(subscriber):
78 self.address = subscriber
79 self.user = self.address.user
80 elif IUser.providedBy(subscriber):
81 self.address = subscriber.preferred_address
82 self.user = subscriber
83 else:
84 raise AssertionError('subscriber is neither an IUser nor IAddress')
85 self.subscriber = subscriber
86 self.pre_verified = pre_verified
87 self.pre_confirmed = pre_confirmed
88 self.pre_approved = pre_approved
90 def _step_sanity_checks(self):
91 # Ensure that we have both an address and a user, even if the address
92 # is not verified. We can't set the preferred address until it is
93 # verified.
94 if self.user is None:
95 # The address has no linked user so create one, link it, and set
96 # the user's preferred address.
97 assert self.address is not None, 'No address or user'
98 self.user = getUtility(IUserManager).make_user(self.address.email)
99 if self.address is None:
100 assert self.user.preferred_address is None, (
101 "Preferred address exists, but wasn't used in constructor")
102 addresses = list(self.user.addresses)
103 if len(addresses) == 0:
104 raise AssertionError('User has no addresses: {}'.format(
105 self.user))
106 # This is rather arbitrary, but we have no choice.
107 self.address = addresses[0]
108 assert self.user is not None and self.address is not None, (
109 'Insane sanity check results')
110 self.push('verification_checks')
112 def _step_verification_checks(self):
113 # Is the address already verified, or is the pre-verified flag set?
114 if self.address.verified_on is None:
115 if self.pre_verified:
116 self.address.verified_on = now()
117 else:
118 # The address being subscribed is not yet verified, so we need
119 # to send a validation email that will also confirm that the
120 # user wants to be subscribed to this mailing list.
121 self.push('send_confirmation')
122 return
123 self.push('confirmation_checks')
125 def _step_confirmation_checks(self):
126 # If the list's subscription policy is open, then the user can be
127 # subscribed right here and now.
128 if self.mlist.subscription_policy is SubscriptionPolicy.open:
129 self.push('do_subscription')
130 return
131 # If we do not need the user's confirmation, then skip to the
132 # moderation checks.
133 if self.mlist.subscription_policy is SubscriptionPolicy.moderate:
134 self.push('moderation_checks')
135 return
136 # If the subscription has been pre-confirmed, then we can skip to the
137 # moderation checks.
138 if self.pre_confirmed:
139 self.push('moderation_checks')
140 return
141 # The user must confirm their subscription.
142 self.push('send_confirmation')
144 def _step_moderation_checks(self):
145 # Does the moderator need to approve the subscription request?
146 assert self.mlist.subscription_policy in (
147 SubscriptionPolicy.moderate,
148 SubscriptionPolicy.confirm_then_moderate)
149 if self.pre_approved:
150 self.push('do_subscription')
151 else:
152 self.push('get_moderator_approval')
154 def _step_do_subscription(self):
155 # We can immediately subscribe the user to the mailing list.
156 self.mlist.subscribe(self.subscriber)
158 def _step_get_moderator_approval(self):
159 # In order to get the moderator's approval, we need to hold the
160 # subscription request in the database
161 request = RequestRecord(
162 self.address.email, self.subscriber.display_name,
163 DeliveryMode.regular, 'en')
164 hold_subscription(self.mlist, request)
166 def _step_send_confirmation(self):
167 self._next.append('moderation_check')
168 self.save()
169 self._next.clear() # stop iteration until we get confirmation
170 # XXX: create the Pendable, send the ConfirmationNeededEvent
171 # (see Registrar.register)
174 @implementer(ISubscriptionService)
175 class SubscriptionService:
176 """Subscription services for the REST API."""
178 __name__ = 'members'
180 def get_members(self):
181 """See `ISubscriptionService`."""
182 # {list_id -> {role -> [members]}}
183 by_list = {}
184 user_manager = getUtility(IUserManager)
185 for member in user_manager.members:
186 by_role = by_list.setdefault(member.list_id, {})
187 members = by_role.setdefault(member.role.name, [])
188 members.append(member)
189 # Flatten into single list sorted as per the interface.
190 all_members = []
191 address_of_member = attrgetter('address.email')
192 for list_id in sorted(by_list):
193 by_role = by_list[list_id]
194 all_members.extend(
195 sorted(by_role.get('owner', []), key=address_of_member))
196 all_members.extend(
197 sorted(by_role.get('moderator', []), key=address_of_member))
198 all_members.extend(
199 sorted(by_role.get('member', []), key=address_of_member))
200 return all_members
202 @dbconnection
203 def get_member(self, store, member_id):
204 """See `ISubscriptionService`."""
205 members = store.query(Member).filter(Member._member_id == member_id)
206 if members.count() == 0:
207 return None
208 else:
209 assert members.count() == 1, 'Too many matching members'
210 return members[0]
212 @dbconnection
213 def find_members(self, store, subscriber=None, list_id=None, role=None):
214 """See `ISubscriptionService`."""
215 # If `subscriber` is a user id, then we'll search for all addresses
216 # which are controlled by the user, otherwise we'll just search for
217 # the given address.
218 user_manager = getUtility(IUserManager)
219 if subscriber is None and list_id is None and role is None:
220 return []
221 # Querying for the subscriber is the most complicated part, because
222 # the parameter can either be an email address or a user id.
223 query = []
224 if subscriber is not None:
225 if isinstance(subscriber, str):
226 # subscriber is an email address.
227 address = user_manager.get_address(subscriber)
228 user = user_manager.get_user(subscriber)
229 # This probably could be made more efficient.
230 if address is None or user is None:
231 return []
232 query.append(or_(Member.address_id == address.id,
233 Member.user_id == user.id))
234 else:
235 # subscriber is a user id.
236 user = user_manager.get_user_by_id(subscriber)
237 address_ids = list(address.id for address in user.addresses
238 if address.id is not None)
239 if len(address_ids) == 0 or user is None:
240 return []
241 query.append(or_(Member.user_id == user.id,
242 Member.address_id.in_(address_ids)))
243 # Calculate the rest of the query expression, which will get And'd
244 # with the Or clause above (if there is one).
245 if list_id is not None:
246 query.append(Member.list_id == list_id)
247 if role is not None:
248 query.append(Member.role == role)
249 results = store.query(Member).filter(and_(*query))
250 return sorted(results, key=_membership_sort_key)
252 def __iter__(self):
253 for member in self.get_members():
254 yield member
256 def join(self, list_id, subscriber,
257 display_name=None,
258 delivery_mode=DeliveryMode.regular,
259 role=MemberRole.member):
260 """See `ISubscriptionService`."""
261 mlist = getUtility(IListManager).get_by_list_id(list_id)
262 if mlist is None:
263 raise NoSuchListError(list_id)
264 # Is the subscriber an email address or user id?
265 if isinstance(subscriber, str):
266 if display_name is None:
267 display_name, at, domain = subscriber.partition('@')
268 return add_member(
269 mlist,
270 RequestRecord(subscriber, display_name, delivery_mode,
271 system_preferences.preferred_language),
272 role)
273 else:
274 # We have to assume it's a UUID.
275 assert isinstance(subscriber, UUID), 'Not a UUID'
276 user = getUtility(IUserManager).get_user_by_id(subscriber)
277 if user is None:
278 raise MissingUserError(subscriber)
279 return mlist.subscribe(user, role)
281 def leave(self, list_id, email):
282 """See `ISubscriptionService`."""
283 mlist = getUtility(IListManager).get_by_list_id(list_id)
284 if mlist is None:
285 raise NoSuchListError(list_id)
286 # XXX for now, no notification or user acknowledgment.
287 delete_member(mlist, email, False, False)
291 def handle_ListDeletingEvent(event):
292 """Delete a mailing list's members when the list is being deleted."""
294 if not isinstance(event, ListDeletingEvent):
295 return
296 # Find all the members still associated with the mailing list.
297 members = getUtility(ISubscriptionService).find_members(
298 list_id=event.mailing_list.list_id)
299 for member in members:
300 member.unsubscribe()