Branch his ready.
[mailman.git] / src / mailman / app / subscriptions.py
blob1593b4d580717a0b2b4893b1fd93d844d39e54ed
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',
27 import uuid
28 import logging
30 from email.utils import formataddr
31 from enum import Enum
32 from datetime import timedelta
33 from mailman.app.membership import delete_member
34 from mailman.app.workflow import Workflow
35 from mailman.core.i18n import _
36 from mailman.database.transaction import dbconnection
37 from mailman.email.message import UserNotification
38 from mailman.interfaces.address import IAddress
39 from mailman.interfaces.bans import IBanManager
40 from mailman.interfaces.listmanager import (
41 IListManager, ListDeletingEvent, NoSuchListError)
42 from mailman.interfaces.mailinglist import SubscriptionPolicy
43 from mailman.interfaces.member import MembershipIsBannedError
44 from mailman.interfaces.pending import IPendable, IPendings
45 from mailman.interfaces.registrar import ConfirmationNeededEvent
46 from mailman.interfaces.subscriptions import ISubscriptionService, TokenOwner
47 from mailman.interfaces.user import IUser
48 from mailman.interfaces.usermanager import IUserManager
49 from mailman.interfaces.workflow import IWorkflowStateManager
50 from mailman.model.member import Member
51 from mailman.utilities.datetime import now
52 from mailman.utilities.i18n import make
53 from operator import attrgetter
54 from sqlalchemy import and_, or_
55 from zope.component import getUtility
56 from zope.event import notify
57 from zope.interface import implementer
60 log = logging.getLogger('mailman.subscribe')
64 def _membership_sort_key(member):
65 """Sort function for find_members().
67 The members are sorted first by unique list id, then by subscribed email
68 address, then by role.
69 """
70 return (member.list_id, member.address.email, member.role.value)
73 class WhichSubscriber(Enum):
74 address = 1
75 user = 2
78 @implementer(IPendable)
79 class Pendable(dict):
80 pass
84 class SubscriptionWorkflow(Workflow):
85 """Workflow of a subscription request."""
87 INITIAL_STATE = 'sanity_checks'
88 SAVE_ATTRIBUTES = (
89 'pre_approved',
90 'pre_confirmed',
91 'pre_verified',
92 'address_key',
93 'subscriber_key',
94 'user_key',
95 'token_owner_key',
98 def __init__(self, mlist, subscriber=None, *,
99 pre_verified=False, pre_confirmed=False, pre_approved=False):
100 super().__init__()
101 self.mlist = mlist
102 self.address = None
103 self.user = None
104 self.which = None
105 self.member = None
106 self._set_token(TokenOwner.no_one)
107 # The subscriber must be either an IUser or IAddress.
108 if IAddress.providedBy(subscriber):
109 self.address = subscriber
110 self.user = self.address.user
111 self.which = WhichSubscriber.address
112 elif IUser.providedBy(subscriber):
113 self.address = subscriber.preferred_address
114 self.user = subscriber
115 self.which = WhichSubscriber.user
116 self.subscriber = subscriber
117 self.pre_verified = pre_verified
118 self.pre_confirmed = pre_confirmed
119 self.pre_approved = pre_approved
121 @property
122 def user_key(self):
123 # For save.
124 return self.user.user_id.hex
126 @user_key.setter
127 def user_key(self, hex_key):
128 # For restore.
129 uid = uuid.UUID(hex_key)
130 self.user = getUtility(IUserManager).get_user_by_id(uid)
131 assert self.user is not None
133 @property
134 def address_key(self):
135 # For save.
136 return self.address.email
138 @address_key.setter
139 def address_key(self, email):
140 # For restore.
141 self.address = getUtility(IUserManager).get_address(email)
142 assert self.address is not None
144 @property
145 def subscriber_key(self):
146 return self.which.value
148 @subscriber_key.setter
149 def subscriber_key(self, key):
150 self.which = WhichSubscriber(key)
152 @property
153 def token_owner_key(self):
154 return self.token_owner.value
156 @token_owner_key.setter
157 def token_owner_key(self, value):
158 self.token_owner = TokenOwner(value)
160 def _set_token(self, token_owner):
161 assert isinstance(token_owner, TokenOwner)
162 pendings = getUtility(IPendings)
163 # Clear out the previous pending token if there is one.
164 if self.token is not None:
165 pendings.confirm(self.token)
166 # Create a new token to prevent replay attacks. It seems like this
167 # would produce the same token, but it won't because the pending adds a
168 # bit of randomization.
169 self.token_owner = token_owner
170 if token_owner is TokenOwner.no_one:
171 self.token = None
172 return
173 pendable = Pendable(
174 list_id=self.mlist.list_id,
175 email=self.address.email,
176 display_name=self.address.display_name,
177 when=now().replace(microsecond=0).isoformat(),
178 token_owner=token_owner.name,
180 self.token = pendings.add(pendable, timedelta(days=3650))
182 def _step_sanity_checks(self):
183 # Ensure that we have both an address and a user, even if the address
184 # is not verified. We can't set the preferred address until it is
185 # verified.
186 if self.user is None:
187 # The address has no linked user so create one, link it, and set
188 # the user's preferred address.
189 assert self.address is not None, 'No address or user'
190 self.user = getUtility(IUserManager).make_user(self.address.email)
191 if self.address is None:
192 assert self.user.preferred_address is None, (
193 "Preferred address exists, but wasn't used in constructor")
194 addresses = list(self.user.addresses)
195 if len(addresses) == 0:
196 raise AssertionError('User has no addresses: {}'.format(
197 self.user))
198 # This is rather arbitrary, but we have no choice.
199 self.address = addresses[0]
200 assert self.user is not None and self.address is not None, (
201 'Insane sanity check results')
202 # Is this email address banned?
203 if IBanManager(self.mlist).is_banned(self.address.email):
204 raise MembershipIsBannedError(self.mlist, self.address.email)
205 # Start out with the subscriber being the token owner.
206 self.push('verification_checks')
208 def _step_verification_checks(self):
209 # Is the address already verified, or is the pre-verified flag set?
210 if self.address.verified_on is None:
211 if self.pre_verified:
212 self.address.verified_on = now()
213 else:
214 # The address being subscribed is not yet verified, so we need
215 # to send a validation email that will also confirm that the
216 # user wants to be subscribed to this mailing list.
217 self.push('send_confirmation')
218 return
219 self.push('confirmation_checks')
221 def _step_confirmation_checks(self):
222 # If the list's subscription policy is open, then the user can be
223 # subscribed right here and now.
224 if self.mlist.subscription_policy is SubscriptionPolicy.open:
225 self.push('do_subscription')
226 return
227 # If we do not need the user's confirmation, then skip to the
228 # moderation checks.
229 if self.mlist.subscription_policy is SubscriptionPolicy.moderate:
230 self.push('moderation_checks')
231 return
232 # If the subscription has been pre-confirmed, then we can skip the
233 # confirmation check can be skipped. If moderator approval is
234 # required we need to check that, otherwise we can go straight to
235 # subscription.
236 if self.pre_confirmed:
237 next_step = ('moderation_checks'
238 if self.mlist.subscription_policy is
239 SubscriptionPolicy.confirm_then_moderate
240 else 'do_subscription')
241 self.push(next_step)
242 return
243 # The user must confirm their subscription.
244 self.push('send_confirmation')
246 def _step_moderation_checks(self):
247 # Does the moderator need to approve the subscription request?
248 assert self.mlist.subscription_policy in (
249 SubscriptionPolicy.moderate,
250 SubscriptionPolicy.confirm_then_moderate,
251 ), self.mlist.subscription_policy
252 if self.pre_approved:
253 self.push('do_subscription')
254 else:
255 self.push('get_moderator_approval')
257 def _step_get_moderator_approval(self):
258 # Here's the next step in the workflow, assuming the moderator
259 # approves of the subscription. If they don't, the workflow and
260 # subscription request will just be thrown away.
261 self._set_token(TokenOwner.moderator)
262 self.push('subscribe_from_restored')
263 self.save()
264 log.info('{}: held subscription request from {}'.format(
265 self.mlist.fqdn_listname, self.address.email))
266 # Possibly send a notification to the list moderators.
267 if self.mlist.admin_immed_notify:
268 subject = _(
269 'New subscription request to $self.mlist.display_name '
270 'from $self.address.email')
271 username = formataddr(
272 (self.subscriber.display_name, self.address.email))
273 text = make('subauth.txt',
274 mailing_list=self.mlist,
275 username=username,
276 listname=self.mlist.fqdn_listname,
278 # This message should appear to come from the <list>-owner so as
279 # to avoid any useless bounce processing.
280 msg = UserNotification(
281 self.mlist.owner_address, self.mlist.owner_address,
282 subject, text, self.mlist.preferred_language)
283 msg.send(self.mlist, tomoderators=True)
284 # The workflow must stop running here.
285 raise StopIteration
287 def _step_subscribe_from_restored(self):
288 # Prevent replay attacks.
289 self._set_token(TokenOwner.no_one)
290 # Restore a little extra state that can't be stored in the database
291 # (because the order of setattr() on restore is indeterminate), then
292 # subscribe the user.
293 if self.which is WhichSubscriber.address:
294 self.subscriber = self.address
295 else:
296 assert self.which is WhichSubscriber.user
297 self.subscriber = self.user
298 self.push('do_subscription')
300 def _step_do_subscription(self):
301 # We can immediately subscribe the user to the mailing list.
302 self.member = self.mlist.subscribe(self.subscriber)
303 # This workflow is done so throw away any associated state.
304 getUtility(IWorkflowStateManager).restore(self.name, self.token)
306 def _step_send_confirmation(self):
307 self._set_token(TokenOwner.subscriber)
308 self.push('do_confirm_verify')
309 self.save()
310 # Triggering this event causes the confirmation message to be sent.
311 notify(ConfirmationNeededEvent(
312 self.mlist, self.token, self.address.email))
313 # Now we wait for the confirmation.
314 raise StopIteration
316 def _step_do_confirm_verify(self):
317 # Restore a little extra state that can't be stored in the database
318 # (because the order of setattr() on restore is indeterminate), then
319 # continue with the confirmation/verification step.
320 if self.which is WhichSubscriber.address:
321 self.subscriber = self.address
322 else:
323 assert self.which is WhichSubscriber.user
324 self.subscriber = self.user
325 # Reset the token so it can't be used in a replay attack.
326 self._set_token(TokenOwner.no_one)
327 # The user has confirmed their subscription request, and also verified
328 # their email address if necessary. This latter needs to be set on the
329 # IAddress, but there's nothing more to do about the confirmation step.
330 # We just continue along with the workflow.
331 if self.address.verified_on is None:
332 self.address.verified_on = now()
333 # The next step depends on the mailing list's subscription policy.
334 next_step = ('moderation_checks'
335 if self.mlist.subscription_policy in (
336 SubscriptionPolicy.moderate,
337 SubscriptionPolicy.confirm_then_moderate,
339 else 'do_subscription')
340 self.push(next_step)
344 @implementer(ISubscriptionService)
345 class SubscriptionService:
346 """Subscription services for the REST API."""
348 __name__ = 'members'
350 def get_members(self):
351 """See `ISubscriptionService`."""
352 # {list_id -> {role -> [members]}}
353 by_list = {}
354 user_manager = getUtility(IUserManager)
355 for member in user_manager.members:
356 by_role = by_list.setdefault(member.list_id, {})
357 members = by_role.setdefault(member.role.name, [])
358 members.append(member)
359 # Flatten into single list sorted as per the interface.
360 all_members = []
361 address_of_member = attrgetter('address.email')
362 for list_id in sorted(by_list):
363 by_role = by_list[list_id]
364 all_members.extend(
365 sorted(by_role.get('owner', []), key=address_of_member))
366 all_members.extend(
367 sorted(by_role.get('moderator', []), key=address_of_member))
368 all_members.extend(
369 sorted(by_role.get('member', []), key=address_of_member))
370 return all_members
372 @dbconnection
373 def get_member(self, store, member_id):
374 """See `ISubscriptionService`."""
375 members = store.query(Member).filter(Member._member_id == member_id)
376 if members.count() == 0:
377 return None
378 else:
379 assert members.count() == 1, 'Too many matching members'
380 return members[0]
382 @dbconnection
383 def find_members(self, store, subscriber=None, list_id=None, role=None):
384 """See `ISubscriptionService`."""
385 # If `subscriber` is a user id, then we'll search for all addresses
386 # which are controlled by the user, otherwise we'll just search for
387 # the given address.
388 user_manager = getUtility(IUserManager)
389 if subscriber is None and list_id is None and role is None:
390 return []
391 # Querying for the subscriber is the most complicated part, because
392 # the parameter can either be an email address or a user id.
393 query = []
394 if subscriber is not None:
395 if isinstance(subscriber, str):
396 # subscriber is an email address.
397 address = user_manager.get_address(subscriber)
398 user = user_manager.get_user(subscriber)
399 # This probably could be made more efficient.
400 if address is None or user is None:
401 return []
402 query.append(or_(Member.address_id == address.id,
403 Member.user_id == user.id))
404 else:
405 # subscriber is a user id.
406 user = user_manager.get_user_by_id(subscriber)
407 address_ids = list(address.id for address in user.addresses
408 if address.id is not None)
409 if len(address_ids) == 0 or user is None:
410 return []
411 query.append(or_(Member.user_id == user.id,
412 Member.address_id.in_(address_ids)))
413 # Calculate the rest of the query expression, which will get And'd
414 # with the Or clause above (if there is one).
415 if list_id is not None:
416 query.append(Member.list_id == list_id)
417 if role is not None:
418 query.append(Member.role == role)
419 results = store.query(Member).filter(and_(*query))
420 return sorted(results, key=_membership_sort_key)
422 def __iter__(self):
423 for member in self.get_members():
424 yield member
426 def leave(self, list_id, email):
427 """See `ISubscriptionService`."""
428 mlist = getUtility(IListManager).get_by_list_id(list_id)
429 if mlist is None:
430 raise NoSuchListError(list_id)
431 # XXX for now, no notification or user acknowledgment.
432 delete_member(mlist, email, False, False)
436 def handle_ListDeletingEvent(event):
437 """Delete a mailing list's members when the list is being deleted."""
439 if not isinstance(event, ListDeletingEvent):
440 return
441 # Find all the members still associated with the mailing list.
442 members = getUtility(ISubscriptionService).find_members(
443 list_id=event.mailing_list.list_id)
444 for member in members:
445 member.unsubscribe()