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',
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_
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.
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'
72 def __init__(self
, mlist
, subscriber
, *,
73 pre_verified
=False, pre_confirmed
=False, pre_approved
=False):
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
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
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(
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()
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')
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')
131 # If we do not need the user's confirmation, then skip to the
133 if self
.mlist
.subscription_policy
is SubscriptionPolicy
.moderate
:
134 self
.push('moderation_checks')
136 # If the subscription has been pre-confirmed, then we can skip to the
138 if self
.pre_confirmed
:
139 self
.push('moderation_checks')
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')
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')
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."""
180 def get_members(self
):
181 """See `ISubscriptionService`."""
182 # {list_id -> {role -> [members]}}
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.
191 address_of_member
= attrgetter('address.email')
192 for list_id
in sorted(by_list
):
193 by_role
= by_list
[list_id
]
195 sorted(by_role
.get('owner', []), key
=address_of_member
))
197 sorted(by_role
.get('moderator', []), key
=address_of_member
))
199 sorted(by_role
.get('member', []), key
=address_of_member
))
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:
209 assert members
.count() == 1, 'Too many matching members'
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
218 user_manager
= getUtility(IUserManager
)
219 if subscriber
is None and list_id
is None and role
is None:
221 # Querying for the subscriber is the most complicated part, because
222 # the parameter can either be an email address or a user id.
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:
232 query
.append(or_(Member
.address_id
== address
.id,
233 Member
.user_id
== user
.id))
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:
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
)
248 query
.append(Member
.role
== role
)
249 results
= store
.query(Member
).filter(and_(*query
))
250 return sorted(results
, key
=_membership_sort_key
)
253 for member
in self
.get_members():
256 def join(self
, list_id
, subscriber
,
258 delivery_mode
=DeliveryMode
.regular
,
259 role
=MemberRole
.member
):
260 """See `ISubscriptionService`."""
261 mlist
= getUtility(IListManager
).get_by_list_id(list_id
)
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('@')
270 RequestRecord(subscriber
, display_name
, delivery_mode
,
271 system_preferences
.preferred_language
),
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
)
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
)
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
):
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
: