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 # 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
:
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
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(
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()
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')
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')
222 # If we do not need the user's confirmation, then skip to the
224 if self
.mlist
.subscription_policy
is SubscriptionPolicy
.moderate
:
225 self
.push('moderation_checks')
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
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')
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')
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')
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
:
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
,
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.
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
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')
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.
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
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')
339 @implementer(ISubscriptionService
)
340 class SubscriptionService
:
341 """Subscription services for the REST API."""
345 def get_members(self
):
346 """See `ISubscriptionService`."""
347 # {list_id -> {role -> [members]}}
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.
356 address_of_member
= attrgetter('address.email')
357 for list_id
in sorted(by_list
):
358 by_role
= by_list
[list_id
]
360 sorted(by_role
.get('owner', []), key
=address_of_member
))
362 sorted(by_role
.get('moderator', []), key
=address_of_member
))
364 sorted(by_role
.get('member', []), key
=address_of_member
))
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:
374 assert members
.count() == 1, 'Too many matching members'
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
383 user_manager
= getUtility(IUserManager
)
384 if subscriber
is None and list_id
is None and role
is None:
386 # Querying for the subscriber is the most complicated part, because
387 # the parameter can either be an email address or a user id.
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:
397 query
.append(or_(Member
.address_id
== address
.id,
398 Member
.user_id
== user
.id))
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:
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
)
413 query
.append(Member
.role
== role
)
414 results
= store
.query(Member
).filter(and_(*query
))
415 return sorted(results
, key
=_membership_sort_key
)
418 for member
in self
.get_members():
421 def leave(self
, list_id
, email
):
422 """See `ISubscriptionService`."""
423 mlist
= getUtility(IListManager
).get_by_list_id(list_id
)
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
):
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
: