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)
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
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."""
21 'SubscriptionService',
22 'SubscriptionWorkflow',
23 'handle_ListDeletingEvent',
30 from email
.utils
import formataddr
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.
70 return (member
.list_id
, member
.address
.email
, member
.role
.value
)
73 class WhichSubscriber(Enum
):
78 @implementer(IPendable
)
84 class SubscriptionWorkflow(Workflow
):
85 """Workflow of a subscription request."""
87 INITIAL_STATE
= 'sanity_checks'
98 def __init__(self
, mlist
, subscriber
=None, *,
99 pre_verified
=False, pre_confirmed
=False, pre_approved
=False):
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
124 return self
.user
.user_id
.hex
127 def user_key(self
, hex_key
):
129 uid
= uuid
.UUID(hex_key
)
130 self
.user
= getUtility(IUserManager
).get_user_by_id(uid
)
131 assert self
.user
is not None
134 def address_key(self
):
136 return self
.address
.email
139 def address_key(self
, email
):
141 self
.address
= getUtility(IUserManager
).get_address(email
)
142 assert self
.address
is not None
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
)
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
:
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
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(
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()
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')
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')
227 # If we do not need the user's confirmation, then skip to the
229 if self
.mlist
.subscription_policy
is SubscriptionPolicy
.moderate
:
230 self
.push('moderation_checks')
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
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')
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')
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')
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
:
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
,
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.
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
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')
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.
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
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')
344 @implementer(ISubscriptionService
)
345 class SubscriptionService
:
346 """Subscription services for the REST API."""
350 def get_members(self
):
351 """See `ISubscriptionService`."""
352 # {list_id -> {role -> [members]}}
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.
361 address_of_member
= attrgetter('address.email')
362 for list_id
in sorted(by_list
):
363 by_role
= by_list
[list_id
]
365 sorted(by_role
.get('owner', []), key
=address_of_member
))
367 sorted(by_role
.get('moderator', []), key
=address_of_member
))
369 sorted(by_role
.get('member', []), key
=address_of_member
))
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:
379 assert members
.count() == 1, 'Too many matching members'
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
388 user_manager
= getUtility(IUserManager
)
389 if subscriber
is None and list_id
is None and role
is None:
391 # Querying for the subscriber is the most complicated part, because
392 # the parameter can either be an email address or a user id.
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:
402 query
.append(or_(Member
.address_id
== address
.id,
403 Member
.user_id
== user
.id))
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:
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
)
418 query
.append(Member
.role
== role
)
419 results
= store
.query(Member
).filter(and_(*query
))
420 return sorted(results
, key
=_membership_sort_key
)
423 for member
in self
.get_members():
426 def leave(self
, list_id
, email
):
427 """See `ISubscriptionService`."""
428 mlist
= getUtility(IListManager
).get_by_list_id(list_id
)
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
):
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
: