Checkpointing.
[mailman.git] / src / mailman / app / subscriptions.py
blob46ce549af4f97e8e324178da5761542e20861774
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 # Create a new token to prevent replay attacks. It seems like this
163 # should produce the same token, but it won't because the pending adds
164 # a bit of randomization.
165 self.token_owner = token_owner
166 if token_owner is TokenOwner.no_one:
167 self.token = None
168 return
169 pendable = Pendable(
170 list_id=self.mlist.list_id,
171 address=self.address.email,
172 hold_date=now().replace(microsecond=0).isoformat(),
173 token_owner=token_owner.name,
175 self.token = getUtility(IPendings).add(pendable, timedelta(days=3650))
177 def _step_sanity_checks(self):
178 # Ensure that we have both an address and a user, even if the address
179 # is not verified. We can't set the preferred address until it is
180 # verified.
181 if self.user is None:
182 # The address has no linked user so create one, link it, and set
183 # the user's preferred address.
184 assert self.address is not None, 'No address or user'
185 self.user = getUtility(IUserManager).make_user(self.address.email)
186 if self.address is None:
187 assert self.user.preferred_address is None, (
188 "Preferred address exists, but wasn't used in constructor")
189 addresses = list(self.user.addresses)
190 if len(addresses) == 0:
191 raise AssertionError('User has no addresses: {}'.format(
192 self.user))
193 # This is rather arbitrary, but we have no choice.
194 self.address = addresses[0]
195 assert self.user is not None and self.address is not None, (
196 'Insane sanity check results')
197 # Is this email address banned?
198 if IBanManager(self.mlist).is_banned(self.address.email):
199 raise MembershipIsBannedError(self.mlist, self.address.email)
200 # Start out with the subscriber being the token owner.
201 self.push('verification_checks')
203 def _step_verification_checks(self):
204 # Is the address already verified, or is the pre-verified flag set?
205 if self.address.verified_on is None:
206 if self.pre_verified:
207 self.address.verified_on = now()
208 else:
209 # The address being subscribed is not yet verified, so we need
210 # to send a validation email that will also confirm that the
211 # user wants to be subscribed to this mailing list.
212 self.push('send_confirmation')
213 return
214 self.push('confirmation_checks')
216 def _step_confirmation_checks(self):
217 # If the list's subscription policy is open, then the user can be
218 # subscribed right here and now.
219 if self.mlist.subscription_policy is SubscriptionPolicy.open:
220 self.push('do_subscription')
221 return
222 # If we do not need the user's confirmation, then skip to the
223 # moderation checks.
224 if self.mlist.subscription_policy is SubscriptionPolicy.moderate:
225 self.push('moderation_checks')
226 return
227 # If the subscription has been pre-confirmed, then we can skip the
228 # confirmation check can be skipped. If moderator approval is
229 # required we need to check that, otherwise we can go straight to
230 # subscription.
231 if self.pre_confirmed:
232 next_step = ('moderation_checks'
233 if self.mlist.subscription_policy is
234 SubscriptionPolicy.confirm_then_moderate
235 else 'do_subscription')
236 self.push(next_step)
237 return
238 # The user must confirm their subscription.
239 self.push('send_confirmation')
241 def _step_moderation_checks(self):
242 # Does the moderator need to approve the subscription request?
243 assert self.mlist.subscription_policy in (
244 SubscriptionPolicy.moderate,
245 SubscriptionPolicy.confirm_then_moderate,
246 ), self.mlist.subscription_policy
247 if self.pre_approved:
248 self.push('do_subscription')
249 else:
250 self.push('get_moderator_approval')
252 def _step_get_moderator_approval(self):
253 # Here's the next step in the workflow, assuming the moderator
254 # approves of the subscription. If they don't, the workflow and
255 # subscription request will just be thrown away.
256 self._set_token(TokenOwner.moderator)
257 self.push('subscribe_from_restored')
258 self.save()
259 log.info('{}: held subscription request from {}'.format(
260 self.mlist.fqdn_listname, self.address.email))
261 # Possibly send a notification to the list moderators.
262 if self.mlist.admin_immed_notify:
263 subject = _(
264 'New subscription request to $self.mlist.display_name '
265 'from $self.address.email')
266 username = formataddr(
267 (self.subscriber.display_name, self.address.email))
268 text = make('subauth.txt',
269 mailing_list=self.mlist,
270 username=username,
271 listname=self.mlist.fqdn_listname,
273 # This message should appear to come from the <list>-owner so as
274 # to avoid any useless bounce processing.
275 msg = UserNotification(
276 self.mlist.owner_address, self.mlist.owner_address,
277 subject, text, self.mlist.preferred_language)
278 msg.send(self.mlist, tomoderators=True)
279 # The workflow must stop running here.
280 raise StopIteration
282 def _step_subscribe_from_restored(self):
283 # Prevent replay attacks.
284 self._set_token(TokenOwner.no_one)
285 # Restore a little extra state that can't be stored in the database
286 # (because the order of setattr() on restore is indeterminate), then
287 # subscribe the user.
288 if self.which is WhichSubscriber.address:
289 self.subscriber = self.address
290 else:
291 assert self.which is WhichSubscriber.user
292 self.subscriber = self.user
293 self.push('do_subscription')
295 def _step_do_subscription(self):
296 # We can immediately subscribe the user to the mailing list.
297 self.member = self.mlist.subscribe(self.subscriber)
298 # This workflow is done so throw away any associated state.
299 getUtility(IWorkflowStateManager).restore(self.name, self.token)
301 def _step_send_confirmation(self):
302 self._set_token(TokenOwner.subscriber)
303 self.push('do_confirm_verify')
304 self.save()
305 # Triggering this event causes the confirmation message to be sent.
306 notify(ConfirmationNeededEvent(
307 self.mlist, self.token, self.address.email))
308 # Now we wait for the confirmation.
309 raise StopIteration
311 def _step_do_confirm_verify(self):
312 # Restore a little extra state that can't be stored in the database
313 # (because the order of setattr() on restore is indeterminate), then
314 # continue with the confirmation/verification step.
315 if self.which is WhichSubscriber.address:
316 self.subscriber = self.address
317 else:
318 assert self.which is WhichSubscriber.user
319 self.subscriber = self.user
320 # Reset the token so it can't be used in a replay attack.
321 self._set_token(TokenOwner.no_one)
322 # The user has confirmed their subscription request, and also verified
323 # their email address if necessary. This latter needs to be set on the
324 # IAddress, but there's nothing more to do about the confirmation step.
325 # We just continue along with the workflow.
326 if self.address.verified_on is None:
327 self.address.verified_on = now()
328 # The next step depends on the mailing list's subscription policy.
329 next_step = ('moderation_checks'
330 if self.mlist.subscription_policy in (
331 SubscriptionPolicy.moderate,
332 SubscriptionPolicy.confirm_then_moderate,
334 else 'do_subscription')
335 self.push(next_step)
339 @implementer(ISubscriptionService)
340 class SubscriptionService:
341 """Subscription services for the REST API."""
343 __name__ = 'members'
345 def get_members(self):
346 """See `ISubscriptionService`."""
347 # {list_id -> {role -> [members]}}
348 by_list = {}
349 user_manager = getUtility(IUserManager)
350 for member in user_manager.members:
351 by_role = by_list.setdefault(member.list_id, {})
352 members = by_role.setdefault(member.role.name, [])
353 members.append(member)
354 # Flatten into single list sorted as per the interface.
355 all_members = []
356 address_of_member = attrgetter('address.email')
357 for list_id in sorted(by_list):
358 by_role = by_list[list_id]
359 all_members.extend(
360 sorted(by_role.get('owner', []), key=address_of_member))
361 all_members.extend(
362 sorted(by_role.get('moderator', []), key=address_of_member))
363 all_members.extend(
364 sorted(by_role.get('member', []), key=address_of_member))
365 return all_members
367 @dbconnection
368 def get_member(self, store, member_id):
369 """See `ISubscriptionService`."""
370 members = store.query(Member).filter(Member._member_id == member_id)
371 if members.count() == 0:
372 return None
373 else:
374 assert members.count() == 1, 'Too many matching members'
375 return members[0]
377 @dbconnection
378 def find_members(self, store, subscriber=None, list_id=None, role=None):
379 """See `ISubscriptionService`."""
380 # If `subscriber` is a user id, then we'll search for all addresses
381 # which are controlled by the user, otherwise we'll just search for
382 # the given address.
383 user_manager = getUtility(IUserManager)
384 if subscriber is None and list_id is None and role is None:
385 return []
386 # Querying for the subscriber is the most complicated part, because
387 # the parameter can either be an email address or a user id.
388 query = []
389 if subscriber is not None:
390 if isinstance(subscriber, str):
391 # subscriber is an email address.
392 address = user_manager.get_address(subscriber)
393 user = user_manager.get_user(subscriber)
394 # This probably could be made more efficient.
395 if address is None or user is None:
396 return []
397 query.append(or_(Member.address_id == address.id,
398 Member.user_id == user.id))
399 else:
400 # subscriber is a user id.
401 user = user_manager.get_user_by_id(subscriber)
402 address_ids = list(address.id for address in user.addresses
403 if address.id is not None)
404 if len(address_ids) == 0 or user is None:
405 return []
406 query.append(or_(Member.user_id == user.id,
407 Member.address_id.in_(address_ids)))
408 # Calculate the rest of the query expression, which will get And'd
409 # with the Or clause above (if there is one).
410 if list_id is not None:
411 query.append(Member.list_id == list_id)
412 if role is not None:
413 query.append(Member.role == role)
414 results = store.query(Member).filter(and_(*query))
415 return sorted(results, key=_membership_sort_key)
417 def __iter__(self):
418 for member in self.get_members():
419 yield member
421 def leave(self, list_id, email):
422 """See `ISubscriptionService`."""
423 mlist = getUtility(IListManager).get_by_list_id(list_id)
424 if mlist is None:
425 raise NoSuchListError(list_id)
426 # XXX for now, no notification or user acknowledgment.
427 delete_member(mlist, email, False, False)
431 def handle_ListDeletingEvent(event):
432 """Delete a mailing list's members when the list is being deleted."""
434 if not isinstance(event, ListDeletingEvent):
435 return
436 # Find all the members still associated with the mailing list.
437 members = getUtility(ISubscriptionService).find_members(
438 list_id=event.mailing_list.list_id)
439 for member in members:
440 member.unsubscribe()