Merge lp:~barry/mailman/subpolicy-2 into lp:mailman
- subpolicy-2
- Merge into 3.0
Status: | Merged |
---|---|
Merged at revision: | 7317 |
Proposed branch: | lp:~barry/mailman/subpolicy-2 |
Merge into: | lp:mailman |
Diff against target: |
3741 lines (+2130/-750) 46 files modified
TODO.rst (+10/-0) src/mailman/app/docs/moderator.rst (+0/-1) src/mailman/app/registrar.py (+33/-99) src/mailman/app/subscriptions.py (+269/-7) src/mailman/app/tests/test_registrar.py (+170/-91) src/mailman/app/tests/test_subscriptions.py (+515/-2) src/mailman/app/tests/test_workflow.py (+128/-0) src/mailman/app/workflow.py (+156/-0) src/mailman/commands/docs/membership.rst (+12/-38) src/mailman/commands/eml_confirm.py (+5/-2) src/mailman/commands/eml_membership.py (+36/-14) src/mailman/commands/tests/test_confirm.py (+5/-6) src/mailman/config/configure.zcml (+11/-5) src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py (+41/-0) src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py (+28/-0) src/mailman/database/transaction.py (+17/-0) src/mailman/interfaces/mailinglist.py (+16/-0) src/mailman/interfaces/member.py (+9/-1) src/mailman/interfaces/pending.py (+5/-2) src/mailman/interfaces/registrar.py (+48/-52) src/mailman/interfaces/roster.py (+18/-3) src/mailman/interfaces/workflow.py (+80/-0) src/mailman/model/docs/membership.rst (+36/-2) src/mailman/model/docs/pending.rst (+10/-0) src/mailman/model/docs/registration.rst (+80/-323) src/mailman/model/mailinglist.py (+2/-1) src/mailman/model/member.py (+4/-0) src/mailman/model/pending.py (+5/-1) src/mailman/model/roster.py (+50/-14) src/mailman/model/tests/test_registrar.py (+0/-64) src/mailman/model/tests/test_roster.py (+51/-1) src/mailman/model/tests/test_usermanager.py (+5/-0) src/mailman/model/tests/test_workflow.py (+148/-0) src/mailman/model/usermanager.py (+0/-1) src/mailman/model/workflow.py (+76/-0) src/mailman/rest/docs/listconf.rst (+3/-0) src/mailman/rest/listconf.py (+3/-1) src/mailman/rest/tests/test_listconf.py (+1/-0) src/mailman/runners/docs/command.rst (+3/-4) src/mailman/runners/tests/test_confirm.py (+2/-2) src/mailman/runners/tests/test_join.py (+6/-2) src/mailman/styles/base.py (+3/-1) src/mailman/templates/en/confirm.txt (+1/-3) src/mailman/templates/en/subauth.txt (+0/-6) src/mailman/utilities/importer.py (+3/-0) src/mailman/utilities/tests/test_import.py (+26/-1) |
To merge this branch: | bzr merge lp:~barry/mailman/subpolicy-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abhilash Raj | Pending | ||
Aurélien Bompard | Pending | ||
Review via email: mp+254511@code.launchpad.net |
Commit message
Description of the change
Here's the subpolicy work, merged from abompard's branch, which is taken from my pre-Python 3 port branch. Here's where the work will continue toward a merge with trunk. All tests pass, and now add_member() usage is reduced to the minimum. I think add_member() is where the subpolicy work will hook into the rest of Mailman.
This is still a WIP.
Aurélien Bompard (abompard) : | # |
Barry Warsaw (barry) wrote : | # |
- 7340. By Barry Warsaw
-
Update TODO.
- 7341. By Barry Warsaw
-
Prevent replay attacks with the confirmation token.
Barry Warsaw (barry) wrote : | # |
Phew! I think this branch is largely done and ready to be merged into trunk. There are still some things I'd like to clean up, and maybe some functionality to add (and bugs to fix?!) but I think this is the minimal must-do for 3.0.
I can't in good conscious ask you to review it again, given its size, but do let me know if anything jumps out at you. If you have a chance to test it, that would be great too. I will merge this to trunk tomorrow.
Preview Diff
1 | === added file 'TODO.rst' |
2 | --- TODO.rst 1970-01-01 00:00:00 +0000 |
3 | +++ TODO.rst 2015-04-15 04:15:28 +0000 |
4 | @@ -0,0 +1,10 @@ |
5 | +* TO DO: |
6 | + - get rid of hold_subscription |
7 | + - subsume handle_subscription |
8 | + - workflow for unsubscription |
9 | + - make sure registration checks IEmailValidator |
10 | + - Test all the various options in eml_membership's get_subscriber() |
11 | + - Bump version to 3.0.0 |
12 | + - Admin notification on membership changes? |
13 | + + admin_notify_mchanges |
14 | + + admin_immed_notify |
15 | |
16 | === modified file 'src/mailman/app/docs/moderator.rst' |
17 | --- src/mailman/app/docs/moderator.rst 2015-03-22 01:32:12 +0000 |
18 | +++ src/mailman/app/docs/moderator.rst 2015-04-15 04:15:28 +0000 |
19 | @@ -424,7 +424,6 @@ |
20 | <BLANKLINE> |
21 | For: iris@example.org |
22 | List: ant@example.com |
23 | - ... |
24 | |
25 | Similarly, the administrator gets notifications on unsubscription requests. |
26 | Jeff is a member of the mailing list, and chooses to unsubscribe. |
27 | |
28 | === modified file 'src/mailman/app/registrar.py' |
29 | --- src/mailman/app/registrar.py 2015-04-06 20:07:04 +0000 |
30 | +++ src/mailman/app/registrar.py 2015-04-15 04:15:28 +0000 |
31 | @@ -25,18 +25,15 @@ |
32 | |
33 | import logging |
34 | |
35 | +from mailman.app.subscriptions import SubscriptionWorkflow |
36 | from mailman.core.i18n import _ |
37 | +from mailman.database.transaction import flush |
38 | from mailman.email.message import UserNotification |
39 | -from mailman.interfaces.address import IEmailValidator |
40 | -from mailman.interfaces.listmanager import IListManager |
41 | -from mailman.interfaces.member import DeliveryMode, MemberRole |
42 | from mailman.interfaces.pending import IPendable, IPendings |
43 | from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar |
44 | from mailman.interfaces.templates import ITemplateLoader |
45 | -from mailman.interfaces.usermanager import IUserManager |
46 | -from mailman.utilities.datetime import now |
47 | +from mailman.interfaces.workflow import IWorkflowStateManager |
48 | from zope.component import getUtility |
49 | -from zope.event import notify |
50 | from zope.interface import implementer |
51 | |
52 | |
53 | @@ -54,96 +51,34 @@ |
54 | class Registrar: |
55 | """Handle registrations and confirmations for subscriptions.""" |
56 | |
57 | - def register(self, mlist, email, display_name=None, delivery_mode=None): |
58 | + def __init__(self, mlist): |
59 | + self._mlist = mlist |
60 | + |
61 | + def register(self, subscriber=None, *, |
62 | + pre_verified=False, pre_confirmed=False, pre_approved=False): |
63 | """See `IRegistrar`.""" |
64 | - if delivery_mode is None: |
65 | - delivery_mode = DeliveryMode.regular |
66 | - # First, do validation on the email address. If the address is |
67 | - # invalid, it will raise an exception, otherwise it just returns. |
68 | - getUtility(IEmailValidator).validate(email) |
69 | - # Create a pendable for the registration. |
70 | - pendable = PendableRegistration( |
71 | - type=PendableRegistration.PEND_KEY, |
72 | - email=email, |
73 | - display_name=display_name, |
74 | - delivery_mode=delivery_mode.name, |
75 | - list_id=mlist.list_id) |
76 | - token = getUtility(IPendings).add(pendable) |
77 | - # We now have everything we need to begin the confirmation dance. |
78 | - # Trigger the event to start the ball rolling, and return the |
79 | - # generated token. |
80 | - notify(ConfirmationNeededEvent(mlist, pendable, token)) |
81 | - return token |
82 | + workflow = SubscriptionWorkflow( |
83 | + self._mlist, subscriber, |
84 | + pre_verified=pre_verified, |
85 | + pre_confirmed=pre_confirmed, |
86 | + pre_approved=pre_approved) |
87 | + list(workflow) |
88 | + return workflow.token |
89 | |
90 | def confirm(self, token): |
91 | """See `IRegistrar`.""" |
92 | - # For convenience |
93 | - pendable = getUtility(IPendings).confirm(token) |
94 | - if pendable is None: |
95 | - return False |
96 | - missing = object() |
97 | - email = pendable.get('email', missing) |
98 | - display_name = pendable.get('display_name', missing) |
99 | - pended_delivery_mode = pendable.get('delivery_mode', 'regular') |
100 | - try: |
101 | - delivery_mode = DeliveryMode[pended_delivery_mode] |
102 | - except ValueError: |
103 | - log.error('Invalid pended delivery_mode for {0}: {1}', |
104 | - email, pended_delivery_mode) |
105 | - delivery_mode = DeliveryMode.regular |
106 | - if pendable.get('type') != PendableRegistration.PEND_KEY: |
107 | - # It seems like it would be very difficult to accurately guess |
108 | - # tokens, or brute force an attack on the SHA1 hash, so we'll just |
109 | - # throw the pendable away in that case. It's possible we'll need |
110 | - # to repend the event or adjust the API to handle this case |
111 | - # better, but for now, the simpler the better. |
112 | - return False |
113 | - # We are going to end up with an IAddress for the verified address |
114 | - # and an IUser linked to this IAddress. See if any of these objects |
115 | - # currently exist in our database. |
116 | - user_manager = getUtility(IUserManager) |
117 | - address = (user_manager.get_address(email) |
118 | - if email is not missing else None) |
119 | - user = (user_manager.get_user(email) |
120 | - if email is not missing else None) |
121 | - # If there is neither an address nor a user matching the confirmed |
122 | - # record, then create the user, which will in turn create the address |
123 | - # and link the two together |
124 | - if address is None: |
125 | - assert user is None, 'How did we get a user but not an address?' |
126 | - user = user_manager.create_user(email, display_name) |
127 | - # Because the database changes haven't been flushed, we can't use |
128 | - # IUserManager.get_address() to find the IAddress just created |
129 | - # under the hood. Instead, iterate through the IUser's addresses, |
130 | - # of which really there should be only one. |
131 | - for address in user.addresses: |
132 | - if address.email == email: |
133 | - break |
134 | - else: |
135 | - raise AssertionError('Could not find expected IAddress') |
136 | - elif user is None: |
137 | - user = user_manager.create_user() |
138 | - user.display_name = display_name |
139 | - user.link(address) |
140 | - else: |
141 | - # The IAddress and linked IUser already exist, so all we need to |
142 | - # do is verify the address. |
143 | - pass |
144 | - address.verified_on = now() |
145 | - # If this registration is tied to a mailing list, subscribe the person |
146 | - # to the list right now. That will generate a SubscriptionEvent, |
147 | - # which can be used to send a welcome message. |
148 | - list_id = pendable.get('list_id') |
149 | - if list_id is not None: |
150 | - mlist = getUtility(IListManager).get_by_list_id(list_id) |
151 | - if mlist is not None: |
152 | - member = mlist.subscribe(address, MemberRole.member) |
153 | - member.preferences.delivery_mode = delivery_mode |
154 | - return True |
155 | + workflow = SubscriptionWorkflow(self._mlist) |
156 | + workflow.token = token |
157 | + workflow.restore() |
158 | + list(workflow) |
159 | + return workflow.token |
160 | |
161 | def discard(self, token): |
162 | - # Throw the record away. |
163 | - getUtility(IPendings).confirm(token) |
164 | + """See `IRegistrar`.""" |
165 | + with flush(): |
166 | + getUtility(IPendings).confirm(token) |
167 | + getUtility(IWorkflowStateManager).discard( |
168 | + SubscriptionWorkflow.__name__, token) |
169 | |
170 | |
171 | |
172 | |
173 | @@ -156,18 +91,17 @@ |
174 | # the Subject header, or they can click on the URL in the body of the |
175 | # message and confirm through the web. |
176 | subject = 'confirm ' + event.token |
177 | - mlist = getUtility(IListManager).get_by_list_id(event.pendable['list_id']) |
178 | - confirm_address = mlist.confirm_address(event.token) |
179 | + confirm_address = event.mlist.confirm_address(event.token) |
180 | # For i18n interpolation. |
181 | - confirm_url = mlist.domain.confirm_url(event.token) |
182 | - email_address = event.pendable['email'] |
183 | - domain_name = mlist.domain.mail_host |
184 | - contact_address = mlist.owner_address |
185 | + confirm_url = event.mlist.domain.confirm_url(event.token) |
186 | + email_address = event.email |
187 | + domain_name = event.mlist.domain.mail_host |
188 | + contact_address = event.mlist.owner_address |
189 | # Send a verification email to the address. |
190 | template = getUtility(ITemplateLoader).get( |
191 | 'mailman:///{0}/{1}/confirm.txt'.format( |
192 | - mlist.fqdn_listname, |
193 | - mlist.preferred_language.code)) |
194 | + event.mlist.fqdn_listname, |
195 | + event.mlist.preferred_language.code)) |
196 | text = _(template) |
197 | msg = UserNotification(email_address, confirm_address, subject, text) |
198 | - msg.send(mlist) |
199 | + msg.send(event.mlist) |
200 | |
201 | === modified file 'src/mailman/app/subscriptions.py' |
202 | --- src/mailman/app/subscriptions.py 2015-03-22 01:32:12 +0000 |
203 | +++ src/mailman/app/subscriptions.py 2015-04-15 04:15:28 +0000 |
204 | @@ -19,26 +19,50 @@ |
205 | |
206 | __all__ = [ |
207 | 'SubscriptionService', |
208 | + 'SubscriptionWorkflow', |
209 | 'handle_ListDeletingEvent', |
210 | ] |
211 | |
212 | |
213 | -from operator import attrgetter |
214 | -from sqlalchemy import and_, or_ |
215 | -from uuid import UUID |
216 | -from zope.component import getUtility |
217 | -from zope.interface import implementer |
218 | - |
219 | + |
220 | +import uuid |
221 | +import logging |
222 | + |
223 | +from email.utils import formataddr |
224 | +from enum import Enum |
225 | +from datetime import timedelta |
226 | from mailman.app.membership import add_member, delete_member |
227 | +from mailman.app.workflow import Workflow |
228 | from mailman.core.constants import system_preferences |
229 | +from mailman.core.i18n import _ |
230 | from mailman.database.transaction import dbconnection |
231 | +from mailman.email.message import UserNotification |
232 | +from mailman.interfaces.address import IAddress |
233 | +from mailman.interfaces.bans import IBanManager |
234 | from mailman.interfaces.listmanager import ( |
235 | IListManager, ListDeletingEvent, NoSuchListError) |
236 | -from mailman.interfaces.member import DeliveryMode, MemberRole |
237 | +from mailman.interfaces.mailinglist import SubscriptionPolicy |
238 | +from mailman.interfaces.member import ( |
239 | + DeliveryMode, MemberRole, MembershipIsBannedError) |
240 | +from mailman.interfaces.pending import IPendable, IPendings |
241 | +from mailman.interfaces.registrar import ConfirmationNeededEvent |
242 | from mailman.interfaces.subscriptions import ( |
243 | ISubscriptionService, MissingUserError, RequestRecord) |
244 | +from mailman.interfaces.user import IUser |
245 | from mailman.interfaces.usermanager import IUserManager |
246 | +from mailman.interfaces.workflow import IWorkflowStateManager |
247 | from mailman.model.member import Member |
248 | +from mailman.utilities.datetime import now |
249 | +from mailman.utilities.i18n import make |
250 | +from operator import attrgetter |
251 | +from sqlalchemy import and_, or_ |
252 | +from uuid import UUID |
253 | +from zope.component import getUtility |
254 | +from zope.event import notify |
255 | +from zope.interface import implementer |
256 | + |
257 | + |
258 | +log = logging.getLogger('mailman.subscribe') |
259 | |
260 | |
261 | |
262 | |
263 | @@ -51,7 +75,245 @@ |
264 | return (member.list_id, member.address.email, member.role.value) |
265 | |
266 | |
267 | +class WhichSubscriber(Enum): |
268 | + address = 1 |
269 | + user = 2 |
270 | + |
271 | + |
272 | +@implementer(IPendable) |
273 | +class Pendable(dict): |
274 | + pass |
275 | + |
276 | + |
277 | |
278 | |
279 | +class SubscriptionWorkflow(Workflow): |
280 | + """Workflow of a subscription request.""" |
281 | + |
282 | + INITIAL_STATE = 'sanity_checks' |
283 | + SAVE_ATTRIBUTES = ( |
284 | + 'pre_approved', |
285 | + 'pre_confirmed', |
286 | + 'pre_verified', |
287 | + 'address_key', |
288 | + 'subscriber_key', |
289 | + 'user_key', |
290 | + ) |
291 | + |
292 | + def __init__(self, mlist, subscriber=None, *, |
293 | + pre_verified=False, pre_confirmed=False, pre_approved=False): |
294 | + super().__init__() |
295 | + self.mlist = mlist |
296 | + self.address = None |
297 | + self.user = None |
298 | + self.which = None |
299 | + # The subscriber must be either an IUser or IAddress. |
300 | + if IAddress.providedBy(subscriber): |
301 | + self.address = subscriber |
302 | + self.user = self.address.user |
303 | + self.which = WhichSubscriber.address |
304 | + elif IUser.providedBy(subscriber): |
305 | + self.address = subscriber.preferred_address |
306 | + self.user = subscriber |
307 | + self.which = WhichSubscriber.user |
308 | + self.subscriber = subscriber |
309 | + self.pre_verified = pre_verified |
310 | + self.pre_confirmed = pre_confirmed |
311 | + self.pre_approved = pre_approved |
312 | + |
313 | + @property |
314 | + def user_key(self): |
315 | + # For save. |
316 | + return self.user.user_id.hex |
317 | + |
318 | + @user_key.setter |
319 | + def user_key(self, hex_key): |
320 | + # For restore. |
321 | + uid = uuid.UUID(hex_key) |
322 | + self.user = getUtility(IUserManager).get_user_by_id(uid) |
323 | + assert self.user is not None |
324 | + |
325 | + @property |
326 | + def address_key(self): |
327 | + # For save. |
328 | + return self.address.email |
329 | + |
330 | + @address_key.setter |
331 | + def address_key(self, email): |
332 | + # For restore. |
333 | + self.address = getUtility(IUserManager).get_address(email) |
334 | + assert self.address is not None |
335 | + |
336 | + @property |
337 | + def subscriber_key(self): |
338 | + return self.which.value |
339 | + |
340 | + @subscriber_key.setter |
341 | + def subscriber_key(self, key): |
342 | + self.which = WhichSubscriber(key) |
343 | + |
344 | + def _step_sanity_checks(self): |
345 | + # Ensure that we have both an address and a user, even if the address |
346 | + # is not verified. We can't set the preferred address until it is |
347 | + # verified. |
348 | + if self.user is None: |
349 | + # The address has no linked user so create one, link it, and set |
350 | + # the user's preferred address. |
351 | + assert self.address is not None, 'No address or user' |
352 | + self.user = getUtility(IUserManager).make_user(self.address.email) |
353 | + if self.address is None: |
354 | + assert self.user.preferred_address is None, ( |
355 | + "Preferred address exists, but wasn't used in constructor") |
356 | + addresses = list(self.user.addresses) |
357 | + if len(addresses) == 0: |
358 | + raise AssertionError('User has no addresses: {}'.format( |
359 | + self.user)) |
360 | + # This is rather arbitrary, but we have no choice. |
361 | + self.address = addresses[0] |
362 | + assert self.user is not None and self.address is not None, ( |
363 | + 'Insane sanity check results') |
364 | + # Is this email address banned? |
365 | + if IBanManager(self.mlist).is_banned(self.address.email): |
366 | + raise MembershipIsBannedError(self.mlist, self.address.email) |
367 | + # Create a pending record. This will give us the hash token we can use |
368 | + # to uniquely name this workflow. |
369 | + pendable = Pendable( |
370 | + list_id=self.mlist.list_id, |
371 | + address=self.address.email, |
372 | + ) |
373 | + self.token = getUtility(IPendings).add(pendable, timedelta(days=3650)) |
374 | + self.push('verification_checks') |
375 | + |
376 | + def _step_verification_checks(self): |
377 | + # Is the address already verified, or is the pre-verified flag set? |
378 | + if self.address.verified_on is None: |
379 | + if self.pre_verified: |
380 | + self.address.verified_on = now() |
381 | + else: |
382 | + # The address being subscribed is not yet verified, so we need |
383 | + # to send a validation email that will also confirm that the |
384 | + # user wants to be subscribed to this mailing list. |
385 | + self.push('send_confirmation') |
386 | + return |
387 | + self.push('confirmation_checks') |
388 | + |
389 | + def _step_confirmation_checks(self): |
390 | + # If the list's subscription policy is open, then the user can be |
391 | + # subscribed right here and now. |
392 | + if self.mlist.subscription_policy is SubscriptionPolicy.open: |
393 | + self.push('do_subscription') |
394 | + return |
395 | + # If we do not need the user's confirmation, then skip to the |
396 | + # moderation checks. |
397 | + if self.mlist.subscription_policy is SubscriptionPolicy.moderate: |
398 | + self.push('moderation_checks') |
399 | + return |
400 | + # If the subscription has been pre-confirmed, then we can skip to the |
401 | + # moderation checks. |
402 | + if self.pre_confirmed: |
403 | + self.push('moderation_checks') |
404 | + return |
405 | + # The user must confirm their subscription. |
406 | + self.push('send_confirmation') |
407 | + |
408 | + def _step_moderation_checks(self): |
409 | + # Does the moderator need to approve the subscription request? |
410 | + assert self.mlist.subscription_policy in ( |
411 | + SubscriptionPolicy.moderate, |
412 | + SubscriptionPolicy.confirm_then_moderate) |
413 | + if self.pre_approved: |
414 | + self.push('do_subscription') |
415 | + else: |
416 | + self.push('get_moderator_approval') |
417 | + |
418 | + def _step_get_moderator_approval(self): |
419 | + # Here's the next step in the workflow, assuming the moderator |
420 | + # approves of the subscription. If they don't, the workflow and |
421 | + # subscription request will just be thrown away. |
422 | + self.push('subscribe_from_restored') |
423 | + self.save() |
424 | + log.info('{}: held subscription request from {}'.format( |
425 | + self.mlist.fqdn_listname, self.address.email)) |
426 | + # Possibly send a notification to the list moderators. |
427 | + if self.mlist.admin_immed_notify: |
428 | + subject = _( |
429 | + 'New subscription request to $self.mlist.display_name ' |
430 | + 'from $self.address.email') |
431 | + username = formataddr( |
432 | + (self.subscriber.display_name, self.address.email)) |
433 | + text = make('subauth.txt', |
434 | + mailing_list=self.mlist, |
435 | + username=username, |
436 | + listname=self.mlist.fqdn_listname, |
437 | + ) |
438 | + # This message should appear to come from the <list>-owner so as |
439 | + # to avoid any useless bounce processing. |
440 | + msg = UserNotification( |
441 | + self.mlist.owner_address, self.mlist.owner_address, |
442 | + subject, text, self.mlist.preferred_language) |
443 | + msg.send(self.mlist, tomoderators=True) |
444 | + # The workflow must stop running here. |
445 | + raise StopIteration |
446 | + |
447 | + def _step_subscribe_from_restored(self): |
448 | + # Restore a little extra state that can't be stored in the database |
449 | + # (because the order of setattr() on restore is indeterminate), then |
450 | + # subscribe the user. |
451 | + if self.which is WhichSubscriber.address: |
452 | + self.subscriber = self.address |
453 | + else: |
454 | + assert self.which is WhichSubscriber.user |
455 | + self.subscriber = self.user |
456 | + self.push('do_subscription') |
457 | + |
458 | + def _step_do_subscription(self): |
459 | + # We can immediately subscribe the user to the mailing list. |
460 | + self.mlist.subscribe(self.subscriber) |
461 | + # This workflow is done so throw away any associated state. |
462 | + getUtility(IWorkflowStateManager).restore(self.name, self.token) |
463 | + self.token = None |
464 | + |
465 | + def _step_send_confirmation(self): |
466 | + self.push('do_confirm_verify') |
467 | + self.save() |
468 | + # Triggering this event causes the confirmation message to be sent. |
469 | + notify(ConfirmationNeededEvent( |
470 | + self.mlist, self.token, self.address.email)) |
471 | + # Now we wait for the confirmation. |
472 | + raise StopIteration |
473 | + |
474 | + def _step_do_confirm_verify(self): |
475 | + # Restore a little extra state that can't be stored in the database |
476 | + # (because the order of setattr() on restore is indeterminate), then |
477 | + # continue with the confirmation/verification step. |
478 | + if self.which is WhichSubscriber.address: |
479 | + self.subscriber = self.address |
480 | + else: |
481 | + assert self.which is WhichSubscriber.user |
482 | + self.subscriber = self.user |
483 | + # Create a new token to prevent replay attacks. It seems like this |
484 | + # should produce the same token, but it won't because the pending adds |
485 | + # a bit of randomization. |
486 | + pendable = Pendable( |
487 | + list_id=self.mlist.list_id, |
488 | + address=self.address.email, |
489 | + ) |
490 | + self.token = getUtility(IPendings).add(pendable, timedelta(days=3650)) |
491 | + # The user has confirmed their subscription request, and also verified |
492 | + # their email address if necessary. This latter needs to be set on the |
493 | + # IAddress, but there's nothing more to do about the confirmation step. |
494 | + # We just continue along with the workflow. |
495 | + if self.address.verified_on is None: |
496 | + self.address.verified_on = now() |
497 | + # The next step depends on the mailing list's subscription policy. |
498 | + next_step = ('moderation_checks' |
499 | + if self.mlist.subscription_policy in ( |
500 | + SubscriptionPolicy.moderate, |
501 | + SubscriptionPolicy.confirm_then_moderate, |
502 | + ) |
503 | + else 'do_subscription') |
504 | + self.push(next_step) |
505 | + |
506 | + |
507 | @implementer(ISubscriptionService) |
508 | class SubscriptionService: |
509 | """Subscription services for the REST API.""" |
510 | |
511 | === renamed file 'src/mailman/app/tests/test_registration.py' => 'src/mailman/app/tests/test_registrar.py' |
512 | --- src/mailman/app/tests/test_registration.py 2015-01-05 01:22:39 +0000 |
513 | +++ src/mailman/app/tests/test_registrar.py 2015-04-15 04:15:28 +0000 |
514 | @@ -18,111 +18,190 @@ |
515 | """Test email address registration.""" |
516 | |
517 | __all__ = [ |
518 | - 'TestEmailValidation', |
519 | - 'TestRegistration', |
520 | + 'TestRegistrar', |
521 | ] |
522 | |
523 | |
524 | import unittest |
525 | |
526 | from mailman.app.lifecycle import create_list |
527 | -from mailman.interfaces.address import InvalidEmailAddressError |
528 | +from mailman.interfaces.mailinglist import SubscriptionPolicy |
529 | from mailman.interfaces.pending import IPendings |
530 | -from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar |
531 | -from mailman.testing.helpers import event_subscribers |
532 | +from mailman.interfaces.registrar import IRegistrar |
533 | +from mailman.interfaces.usermanager import IUserManager |
534 | from mailman.testing.layers import ConfigLayer |
535 | +from mailman.utilities.datetime import now |
536 | from zope.component import getUtility |
537 | |
538 | |
539 | |
540 | |
541 | -class TestEmailValidation(unittest.TestCase): |
542 | - """Test basic email validation.""" |
543 | - |
544 | - layer = ConfigLayer |
545 | - |
546 | - def setUp(self): |
547 | - self.registrar = getUtility(IRegistrar) |
548 | - self.mlist = create_list('alpha@example.com') |
549 | - |
550 | - def test_empty_string_is_invalid(self): |
551 | - self.assertRaises(InvalidEmailAddressError, |
552 | - self.registrar.register, self.mlist, |
553 | - '') |
554 | - |
555 | - def test_no_spaces_allowed(self): |
556 | - self.assertRaises(InvalidEmailAddressError, |
557 | - self.registrar.register, self.mlist, |
558 | - 'some name@example.com') |
559 | - |
560 | - def test_no_angle_brackets(self): |
561 | - self.assertRaises(InvalidEmailAddressError, |
562 | - self.registrar.register, self.mlist, |
563 | - '<script>@example.com') |
564 | - |
565 | - def test_ascii_only(self): |
566 | - self.assertRaises(InvalidEmailAddressError, |
567 | - self.registrar.register, self.mlist, |
568 | - '\xa0@example.com') |
569 | - |
570 | - def test_domain_required(self): |
571 | - self.assertRaises(InvalidEmailAddressError, |
572 | - self.registrar.register, self.mlist, |
573 | - 'noatsign') |
574 | - |
575 | - def test_full_domain_required(self): |
576 | - self.assertRaises(InvalidEmailAddressError, |
577 | - self.registrar.register, self.mlist, |
578 | - 'nodom@ain') |
579 | - |
580 | - |
581 | - |
582 | |
583 | -class TestRegistration(unittest.TestCase): |
584 | +class TestRegistrar(unittest.TestCase): |
585 | """Test registration.""" |
586 | |
587 | layer = ConfigLayer |
588 | |
589 | def setUp(self): |
590 | - self.registrar = getUtility(IRegistrar) |
591 | - self.mlist = create_list('alpha@example.com') |
592 | - |
593 | - def test_confirmation_event_received(self): |
594 | - # Registering an email address generates an event. |
595 | - def capture_event(event): |
596 | - self.assertIsInstance(event, ConfirmationNeededEvent) |
597 | - with event_subscribers(capture_event): |
598 | - self.registrar.register(self.mlist, 'anne@example.com') |
599 | - |
600 | - def test_event_mlist(self): |
601 | - # The event has a reference to the mailing list being subscribed to. |
602 | - def capture_event(event): |
603 | - self.assertIs(event.mlist, self.mlist) |
604 | - with event_subscribers(capture_event): |
605 | - self.registrar.register(self.mlist, 'anne@example.com') |
606 | - |
607 | - def test_event_pendable(self): |
608 | - # The event has an IPendable which contains additional information. |
609 | - def capture_event(event): |
610 | - pendable = event.pendable |
611 | - self.assertEqual(pendable['type'], 'registration') |
612 | - self.assertEqual(pendable['email'], 'anne@example.com') |
613 | - # The key is present, but the value is None. |
614 | - self.assertIsNone(pendable['display_name']) |
615 | - # The default is regular delivery. |
616 | - self.assertEqual(pendable['delivery_mode'], 'regular') |
617 | - self.assertEqual(pendable['list_id'], 'alpha.example.com') |
618 | - with event_subscribers(capture_event): |
619 | - self.registrar.register(self.mlist, 'anne@example.com') |
620 | - |
621 | - def test_token(self): |
622 | - # Registering the email address returns a token, and this token links |
623 | - # back to the pendable. |
624 | - captured_events = [] |
625 | - def capture_event(event): |
626 | - captured_events.append(event) |
627 | - with event_subscribers(capture_event): |
628 | - token = self.registrar.register(self.mlist, 'anne@example.com') |
629 | - self.assertEqual(len(captured_events), 1) |
630 | - event = captured_events[0] |
631 | - self.assertEqual(event.token, token) |
632 | - pending = getUtility(IPendings).confirm(token) |
633 | - self.assertEqual(pending, event.pendable) |
634 | + self._mlist = create_list('ant@example.com') |
635 | + self._registrar = IRegistrar(self._mlist) |
636 | + self._pendings = getUtility(IPendings) |
637 | + self._anne = getUtility(IUserManager).create_address( |
638 | + 'anne@example.com') |
639 | + |
640 | + def test_unique_token(self): |
641 | + # Registering a subscription request provides a unique token associated |
642 | + # with a pendable. |
643 | + self.assertEqual(self._pendings.count(), 0) |
644 | + token = self._registrar.register(self._anne) |
645 | + self.assertIsNotNone(token) |
646 | + self.assertEqual(self._pendings.count(), 1) |
647 | + record = self._pendings.confirm(token, expunge=False) |
648 | + self.assertEqual(record['list_id'], self._mlist.list_id) |
649 | + self.assertEqual(record['address'], 'anne@example.com') |
650 | + |
651 | + def test_no_token(self): |
652 | + # Registering a subscription request where no confirmation or |
653 | + # moderation steps are needed, leaves us with no token, since there's |
654 | + # nothing more to do. |
655 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
656 | + self._anne.verified_on = now() |
657 | + token = self._registrar.register(self._anne) |
658 | + self.assertIsNone(token) |
659 | + record = self._pendings.confirm(token, expunge=False) |
660 | + self.assertIsNone(record) |
661 | + |
662 | + def test_is_subscribed(self): |
663 | + # Where no confirmation or moderation steps are needed, registration |
664 | + # happens immediately. |
665 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
666 | + self._anne.verified_on = now() |
667 | + status = self._registrar.register(self._anne) |
668 | + self.assertIsNone(status) |
669 | + member = self._mlist.regular_members.get_member('anne@example.com') |
670 | + self.assertEqual(member.address, self._anne) |
671 | + |
672 | + def test_no_such_token(self): |
673 | + # Given a token which is not in the database, a LookupError is raised. |
674 | + self._registrar.register(self._anne) |
675 | + self.assertRaises(LookupError, self._registrar.confirm, 'not-a-token') |
676 | + |
677 | + def test_confirm_because_verify(self): |
678 | + # We have a subscription request which requires the user to confirm |
679 | + # (because she does not have a verified address), but not the moderator |
680 | + # to approve. Running the workflow gives us a token. Confirming the |
681 | + # token subscribes the user. |
682 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
683 | + token = self._registrar.register(self._anne) |
684 | + self.assertIsNotNone(token) |
685 | + member = self._mlist.regular_members.get_member('anne@example.com') |
686 | + self.assertIsNone(member) |
687 | + # Now confirm the subscription. |
688 | + self._registrar.confirm(token) |
689 | + member = self._mlist.regular_members.get_member('anne@example.com') |
690 | + self.assertEqual(member.address, self._anne) |
691 | + |
692 | + def test_confirm_because_confirm(self): |
693 | + # We have a subscription request which requires the user to confirm |
694 | + # (because of list policy), but not the moderator to approve. Running |
695 | + # the workflow gives us a token. Confirming the token subscribes the |
696 | + # user. |
697 | + self._mlist.subscription_policy = SubscriptionPolicy.confirm |
698 | + self._anne.verified_on = now() |
699 | + token = self._registrar.register(self._anne) |
700 | + self.assertIsNotNone(token) |
701 | + member = self._mlist.regular_members.get_member('anne@example.com') |
702 | + self.assertIsNone(member) |
703 | + # Now confirm the subscription. |
704 | + self._registrar.confirm(token) |
705 | + member = self._mlist.regular_members.get_member('anne@example.com') |
706 | + self.assertEqual(member.address, self._anne) |
707 | + |
708 | + def test_confirm_because_moderation(self): |
709 | + # We have a subscription request which requires the moderator to |
710 | + # approve. Running the workflow gives us a token. Confirming the |
711 | + # token subscribes the user. |
712 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
713 | + self._anne.verified_on = now() |
714 | + token = self._registrar.register(self._anne) |
715 | + self.assertIsNotNone(token) |
716 | + member = self._mlist.regular_members.get_member('anne@example.com') |
717 | + self.assertIsNone(member) |
718 | + # Now confirm the subscription. |
719 | + self._registrar.confirm(token) |
720 | + member = self._mlist.regular_members.get_member('anne@example.com') |
721 | + self.assertEqual(member.address, self._anne) |
722 | + |
723 | + def test_confirm_because_confirm_then_moderation(self): |
724 | + # We have a subscription request which requires the user to confirm |
725 | + # (because she does not have a verified address) and the moderator to |
726 | + # approve. Running the workflow gives us a token. Confirming the |
727 | + # token runs the workflow a little farther, but still gives us a |
728 | + # token. Confirming again subscribes the user. |
729 | + self._mlist.subscription_policy = \ |
730 | + SubscriptionPolicy.confirm_then_moderate |
731 | + self._anne.verified_on = now() |
732 | + # Runs until subscription confirmation. |
733 | + token = self._registrar.register(self._anne) |
734 | + self.assertIsNotNone(token) |
735 | + member = self._mlist.regular_members.get_member('anne@example.com') |
736 | + self.assertIsNone(member) |
737 | + # Now confirm the subscription, and wait for the moderator to approve |
738 | + # the subscription. She is still not subscribed. |
739 | + new_token = self._registrar.confirm(token) |
740 | + # The new token, used for the moderator to approve the message, is not |
741 | + # the same as the old token. |
742 | + self.assertNotEqual(new_token, token) |
743 | + member = self._mlist.regular_members.get_member('anne@example.com') |
744 | + self.assertIsNone(member) |
745 | + # Confirm once more, this time as the moderator approving the |
746 | + # subscription. Now she's a member. |
747 | + self._registrar.confirm(new_token) |
748 | + member = self._mlist.regular_members.get_member('anne@example.com') |
749 | + self.assertEqual(member.address, self._anne) |
750 | + |
751 | + def test_confirm_then_moderate_with_different_tokens(self): |
752 | + # Ensure that the confirmation token the user sees when they have to |
753 | + # confirm their subscription is different than the token the moderator |
754 | + # sees when they approve the subscription. This prevents the user |
755 | + # from using a replay attack to subvert moderator approval. |
756 | + self._mlist.subscription_policy = \ |
757 | + SubscriptionPolicy.confirm_then_moderate |
758 | + self._anne.verified_on = now() |
759 | + # Runs until subscription confirmation. |
760 | + token = self._registrar.register(self._anne) |
761 | + self.assertIsNotNone(token) |
762 | + member = self._mlist.regular_members.get_member('anne@example.com') |
763 | + self.assertIsNone(member) |
764 | + # Now confirm the subscription, and wait for the moderator to approve |
765 | + # the subscription. She is still not subscribed. |
766 | + new_token = self._registrar.confirm(token) |
767 | + # The status is not true because the user has not yet been subscribed |
768 | + # to the mailing list. |
769 | + self.assertIsNotNone(new_token) |
770 | + member = self._mlist.regular_members.get_member('anne@example.com') |
771 | + self.assertIsNone(member) |
772 | + # The new token is different than the old token. |
773 | + self.assertNotEqual(token, new_token) |
774 | + # Trying to confirm with the old token does not work. |
775 | + self.assertRaises(LookupError, self._registrar.confirm, token) |
776 | + # Confirm once more, this time with the new token, as the moderator |
777 | + # approving the subscription. Now she's a member. |
778 | + done_token = self._registrar.confirm(new_token) |
779 | + # The token is None, signifying that the member has been subscribed. |
780 | + self.assertIsNone(done_token) |
781 | + member = self._mlist.regular_members.get_member('anne@example.com') |
782 | + self.assertEqual(member.address, self._anne) |
783 | + |
784 | + def test_discard_waiting_for_confirmation(self): |
785 | + # While waiting for a user to confirm their subscription, we discard |
786 | + # the workflow. |
787 | + self._mlist.subscription_policy = SubscriptionPolicy.confirm |
788 | + self._anne.verified_on = now() |
789 | + # Runs until subscription confirmation. |
790 | + token = self._registrar.register(self._anne) |
791 | + self.assertIsNotNone(token) |
792 | + member = self._mlist.regular_members.get_member('anne@example.com') |
793 | + self.assertIsNone(member) |
794 | + # Now discard the subscription request. |
795 | + self._registrar.discard(token) |
796 | + # Trying to confirm the token now results in an exception. |
797 | + self.assertRaises(LookupError, self._registrar.confirm, token) |
798 | |
799 | === modified file 'src/mailman/app/tests/test_subscriptions.py' |
800 | --- src/mailman/app/tests/test_subscriptions.py 2015-01-05 01:22:39 +0000 |
801 | +++ src/mailman/app/tests/test_subscriptions.py 2015-04-15 04:15:28 +0000 |
802 | @@ -18,7 +18,8 @@ |
803 | """Tests for the subscription service.""" |
804 | |
805 | __all__ = [ |
806 | - 'TestJoin' |
807 | + 'TestJoin', |
808 | + 'TestSubscriptionWorkflow', |
809 | ] |
810 | |
811 | |
812 | @@ -26,11 +27,20 @@ |
813 | import unittest |
814 | |
815 | from mailman.app.lifecycle import create_list |
816 | +from mailman.app.subscriptions import SubscriptionWorkflow |
817 | from mailman.interfaces.address import InvalidEmailAddressError |
818 | -from mailman.interfaces.member import MemberRole, MissingPreferredAddressError |
819 | +from mailman.interfaces.bans import IBanManager |
820 | +from mailman.interfaces.member import ( |
821 | + MemberRole, MembershipIsBannedError, MissingPreferredAddressError) |
822 | +from mailman.interfaces.pending import IPendings |
823 | from mailman.interfaces.subscriptions import ( |
824 | MissingUserError, ISubscriptionService) |
825 | +from mailman.testing.helpers import LogFileMark, get_queue_messages |
826 | from mailman.testing.layers import ConfigLayer |
827 | +from mailman.interfaces.mailinglist import SubscriptionPolicy |
828 | +from mailman.interfaces.usermanager import IUserManager |
829 | +from mailman.utilities.datetime import now |
830 | +from unittest.mock import patch |
831 | from zope.component import getUtility |
832 | |
833 | |
834 | @@ -65,3 +75,506 @@ |
835 | self._service.join, |
836 | 'test.example.com', anne.user.user_id, |
837 | role=MemberRole.owner) |
838 | + |
839 | + |
840 | + |
841 | |
842 | +class TestSubscriptionWorkflow(unittest.TestCase): |
843 | + layer = ConfigLayer |
844 | + maxDiff = None |
845 | + |
846 | + def setUp(self): |
847 | + self._mlist = create_list('test@example.com') |
848 | + self._mlist.admin_immed_notify = False |
849 | + self._anne = 'anne@example.com' |
850 | + self._user_manager = getUtility(IUserManager) |
851 | + |
852 | + def test_user_or_address_required(self): |
853 | + # The `subscriber` attribute must be a user or address. |
854 | + workflow = SubscriptionWorkflow(self._mlist) |
855 | + self.assertRaises(AssertionError, list, workflow) |
856 | + |
857 | + def test_sanity_checks_address(self): |
858 | + # Ensure that the sanity check phase, when given an IAddress, ends up |
859 | + # with a linked user. |
860 | + anne = self._user_manager.create_address(self._anne) |
861 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
862 | + self.assertIsNotNone(workflow.address) |
863 | + self.assertIsNone(workflow.user) |
864 | + workflow.run_thru('sanity_checks') |
865 | + self.assertIsNotNone(workflow.address) |
866 | + self.assertIsNotNone(workflow.user) |
867 | + self.assertEqual(list(workflow.user.addresses)[0].email, self._anne) |
868 | + |
869 | + def test_sanity_checks_user_with_preferred_address(self): |
870 | + # Ensure that the sanity check phase, when given an IUser with a |
871 | + # preferred address, ends up with an address. |
872 | + anne = self._user_manager.make_user(self._anne) |
873 | + address = list(anne.addresses)[0] |
874 | + address.verified_on = now() |
875 | + anne.preferred_address = address |
876 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
877 | + # The constructor sets workflow.address because the user has a |
878 | + # preferred address. |
879 | + self.assertEqual(workflow.address, address) |
880 | + self.assertEqual(workflow.user, anne) |
881 | + workflow.run_thru('sanity_checks') |
882 | + self.assertEqual(workflow.address, address) |
883 | + self.assertEqual(workflow.user, anne) |
884 | + |
885 | + def test_sanity_checks_user_without_preferred_address(self): |
886 | + # Ensure that the sanity check phase, when given a user without a |
887 | + # preferred address, but with at least one linked address, gets an |
888 | + # address. |
889 | + anne = self._user_manager.make_user(self._anne) |
890 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
891 | + self.assertIsNone(workflow.address) |
892 | + self.assertEqual(workflow.user, anne) |
893 | + workflow.run_thru('sanity_checks') |
894 | + self.assertIsNotNone(workflow.address) |
895 | + self.assertEqual(workflow.user, anne) |
896 | + |
897 | + def test_sanity_checks_user_with_multiple_linked_addresses(self): |
898 | + # Ensure that the santiy check phase, when given a user without a |
899 | + # preferred address, but with multiple linked addresses, gets of of |
900 | + # those addresses (exactly which one is undefined). |
901 | + anne = self._user_manager.make_user(self._anne) |
902 | + anne.link(self._user_manager.create_address('anne@example.net')) |
903 | + anne.link(self._user_manager.create_address('anne@example.org')) |
904 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
905 | + self.assertIsNone(workflow.address) |
906 | + self.assertEqual(workflow.user, anne) |
907 | + workflow.run_thru('sanity_checks') |
908 | + self.assertIn(workflow.address.email, ['anne@example.com', |
909 | + 'anne@example.net', |
910 | + 'anne@example.org']) |
911 | + self.assertEqual(workflow.user, anne) |
912 | + |
913 | + def test_sanity_checks_user_without_addresses(self): |
914 | + # It is an error to try to subscribe a user with no linked addresses. |
915 | + user = self._user_manager.create_user() |
916 | + workflow = SubscriptionWorkflow(self._mlist, user) |
917 | + self.assertRaises(AssertionError, workflow.run_thru, 'sanity_checks') |
918 | + |
919 | + def test_sanity_checks_globally_banned_address(self): |
920 | + # An exception is raised if the address is globally banned. |
921 | + anne = self._user_manager.create_address(self._anne) |
922 | + IBanManager(None).ban(self._anne) |
923 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
924 | + self.assertRaises(MembershipIsBannedError, list, workflow) |
925 | + |
926 | + def test_sanity_checks_banned_address(self): |
927 | + # An exception is raised if the address is banned by the mailing list. |
928 | + anne = self._user_manager.create_address(self._anne) |
929 | + IBanManager(self._mlist).ban(self._anne) |
930 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
931 | + self.assertRaises(MembershipIsBannedError, list, workflow) |
932 | + |
933 | + def test_verification_checks_with_verified_address(self): |
934 | + # When the address is already verified, we skip straight to the |
935 | + # confirmation checks. |
936 | + anne = self._user_manager.create_address(self._anne) |
937 | + anne.verified_on = now() |
938 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
939 | + workflow.run_thru('verification_checks') |
940 | + with patch.object(workflow, '_step_confirmation_checks') as step: |
941 | + next(workflow) |
942 | + step.assert_called_once_with() |
943 | + |
944 | + def test_verification_checks_with_pre_verified_address(self): |
945 | + # When the address is not yet verified, but the pre-verified flag is |
946 | + # passed to the workflow, we skip to the confirmation checks. |
947 | + anne = self._user_manager.create_address(self._anne) |
948 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
949 | + workflow.run_thru('verification_checks') |
950 | + with patch.object(workflow, '_step_confirmation_checks') as step: |
951 | + next(workflow) |
952 | + step.assert_called_once_with() |
953 | + # And now the address is verified. |
954 | + self.assertIsNotNone(anne.verified_on) |
955 | + |
956 | + def test_verification_checks_confirmation_needed(self): |
957 | + # The address is neither verified, nor is the pre-verified flag set. |
958 | + # A confirmation message must be sent to the user which will also |
959 | + # verify their address. |
960 | + anne = self._user_manager.create_address(self._anne) |
961 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
962 | + workflow.run_thru('verification_checks') |
963 | + with patch.object(workflow, '_step_send_confirmation') as step: |
964 | + next(workflow) |
965 | + step.assert_called_once_with() |
966 | + # The address still hasn't been verified. |
967 | + self.assertIsNone(anne.verified_on) |
968 | + |
969 | + def test_confirmation_checks_open_list(self): |
970 | + # A subscription to an open list does not need to be confirmed or |
971 | + # moderated. |
972 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
973 | + anne = self._user_manager.create_address(self._anne) |
974 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
975 | + workflow.run_thru('confirmation_checks') |
976 | + with patch.object(workflow, '_step_do_subscription') as step: |
977 | + next(workflow) |
978 | + step.assert_called_once_with() |
979 | + |
980 | + def test_confirmation_checks_no_user_confirmation_needed(self): |
981 | + # A subscription to a list which does not need user confirmation skips |
982 | + # to the moderation checks. |
983 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
984 | + anne = self._user_manager.create_address(self._anne) |
985 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
986 | + workflow.run_thru('confirmation_checks') |
987 | + with patch.object(workflow, '_step_moderation_checks') as step: |
988 | + next(workflow) |
989 | + step.assert_called_once_with() |
990 | + |
991 | + def test_confirmation_checks_confirm_pre_confirmed(self): |
992 | + # The subscription policy requires user confirmation, but their |
993 | + # subscription is pre-confirmed. |
994 | + self._mlist.subscription_policy = SubscriptionPolicy.confirm |
995 | + anne = self._user_manager.create_address(self._anne) |
996 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
997 | + pre_verified=True, |
998 | + pre_confirmed=True) |
999 | + workflow.run_thru('confirmation_checks') |
1000 | + with patch.object(workflow, '_step_moderation_checks') as step: |
1001 | + next(workflow) |
1002 | + step.assert_called_once_with() |
1003 | + |
1004 | + def test_confirmation_checks_confirm_and_moderate_pre_confirmed(self): |
1005 | + # The subscription policy requires user confirmation and moderation, |
1006 | + # but their subscription is pre-confirmed. |
1007 | + self._mlist.subscription_policy = \ |
1008 | + SubscriptionPolicy.confirm_then_moderate |
1009 | + anne = self._user_manager.create_address(self._anne) |
1010 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1011 | + pre_verified=True, |
1012 | + pre_confirmed=True) |
1013 | + workflow.run_thru('confirmation_checks') |
1014 | + with patch.object(workflow, '_step_moderation_checks') as step: |
1015 | + next(workflow) |
1016 | + step.assert_called_once_with() |
1017 | + |
1018 | + def test_confirmation_checks_confirmation_needed(self): |
1019 | + # The subscription policy requires confirmation and the subscription |
1020 | + # is not pre-confirmed. |
1021 | + self._mlist.subscription_policy = SubscriptionPolicy.confirm |
1022 | + anne = self._user_manager.create_address(self._anne) |
1023 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
1024 | + workflow.run_thru('confirmation_checks') |
1025 | + with patch.object(workflow, '_step_send_confirmation') as step: |
1026 | + next(workflow) |
1027 | + step.assert_called_once_with() |
1028 | + |
1029 | + def test_confirmation_checks_moderate_confirmation_needed(self): |
1030 | + # The subscription policy requires confirmation and moderation, and the |
1031 | + # subscription is not pre-confirmed. |
1032 | + self._mlist.subscription_policy = \ |
1033 | + SubscriptionPolicy.confirm_then_moderate |
1034 | + anne = self._user_manager.create_address(self._anne) |
1035 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
1036 | + workflow.run_thru('confirmation_checks') |
1037 | + with patch.object(workflow, '_step_send_confirmation') as step: |
1038 | + next(workflow) |
1039 | + step.assert_called_once_with() |
1040 | + |
1041 | + def test_moderation_checks_pre_approved(self): |
1042 | + # The subscription is pre-approved by the moderator. |
1043 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
1044 | + anne = self._user_manager.create_address(self._anne) |
1045 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1046 | + pre_verified=True, |
1047 | + pre_approved=True) |
1048 | + workflow.run_thru('moderation_checks') |
1049 | + with patch.object(workflow, '_step_do_subscription') as step: |
1050 | + next(workflow) |
1051 | + step.assert_called_once_with() |
1052 | + |
1053 | + def test_moderation_checks_approval_required(self): |
1054 | + # The moderator must approve the subscription. |
1055 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
1056 | + anne = self._user_manager.create_address(self._anne) |
1057 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
1058 | + workflow.run_thru('moderation_checks') |
1059 | + with patch.object(workflow, '_step_get_moderator_approval') as step: |
1060 | + next(workflow) |
1061 | + step.assert_called_once_with() |
1062 | + |
1063 | + def test_do_subscription(self): |
1064 | + # An open subscription policy plus a pre-verified address means the |
1065 | + # user gets subscribed to the mailing list without any further |
1066 | + # confirmations or approvals. |
1067 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
1068 | + anne = self._user_manager.create_address(self._anne) |
1069 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
1070 | + # Consume the entire state machine. |
1071 | + list(workflow) |
1072 | + # Anne is now a member of the mailing list. |
1073 | + member = self._mlist.regular_members.get_member(self._anne) |
1074 | + self.assertEqual(member.address, anne) |
1075 | + |
1076 | + def test_do_subscription_pre_approved(self): |
1077 | + # An moderation-requiring subscription policy plus a pre-verified and |
1078 | + # pre-approved address means the user gets subscribed to the mailing |
1079 | + # list without any further confirmations or approvals. |
1080 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
1081 | + anne = self._user_manager.create_address(self._anne) |
1082 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1083 | + pre_verified=True, |
1084 | + pre_approved=True) |
1085 | + # Consume the entire state machine. |
1086 | + list(workflow) |
1087 | + # Anne is now a member of the mailing list. |
1088 | + member = self._mlist.regular_members.get_member(self._anne) |
1089 | + self.assertEqual(member.address, anne) |
1090 | + |
1091 | + def test_do_subscription_pre_approved_pre_confirmed(self): |
1092 | + # An moderation-requiring subscription policy plus a pre-verified and |
1093 | + # pre-approved address means the user gets subscribed to the mailing |
1094 | + # list without any further confirmations or approvals. |
1095 | + self._mlist.subscription_policy = \ |
1096 | + SubscriptionPolicy.confirm_then_moderate |
1097 | + anne = self._user_manager.create_address(self._anne) |
1098 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1099 | + pre_verified=True, |
1100 | + pre_confirmed=True, |
1101 | + pre_approved=True) |
1102 | + # Consume the entire state machine. |
1103 | + list(workflow) |
1104 | + # Anne is now a member of the mailing list. |
1105 | + member = self._mlist.regular_members.get_member(self._anne) |
1106 | + self.assertEqual(member.address, anne) |
1107 | + |
1108 | + def test_do_subscription_cleanups(self): |
1109 | + # Once the user is subscribed, the token, and its associated pending |
1110 | + # database record will be removed from the database. |
1111 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
1112 | + anne = self._user_manager.create_address(self._anne) |
1113 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1114 | + pre_verified=True, |
1115 | + pre_confirmed=True, |
1116 | + pre_approved=True) |
1117 | + # Cache the token. |
1118 | + token = workflow.token |
1119 | + # Consume the entire state machine. |
1120 | + list(workflow) |
1121 | + # Anne is now a member of the mailing list. |
1122 | + member = self._mlist.regular_members.get_member(self._anne) |
1123 | + self.assertEqual(member.address, anne) |
1124 | + # The workflow is done, so it has no token. |
1125 | + self.assertIsNone(workflow.token) |
1126 | + # The pendable associated with the token has been evicted. |
1127 | + self.assertIsNone(getUtility(IPendings).confirm(token, expunge=False)) |
1128 | + # There is no saved workflow associated with the token. This shows up |
1129 | + # as an exception when we try to restore the workflow. |
1130 | + new_workflow = SubscriptionWorkflow(self._mlist) |
1131 | + new_workflow.token = token |
1132 | + self.assertRaises(LookupError, new_workflow.restore) |
1133 | + |
1134 | + def test_moderator_approves(self): |
1135 | + # The workflow runs until moderator approval is required, at which |
1136 | + # point the workflow is saved. Once the moderator approves, the |
1137 | + # workflow resumes and the user is subscribed. |
1138 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
1139 | + anne = self._user_manager.create_address(self._anne) |
1140 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1141 | + pre_verified=True, |
1142 | + pre_confirmed=True) |
1143 | + # Consume the entire state machine. |
1144 | + list(workflow) |
1145 | + # The user is not currently subscribed to the mailing list. |
1146 | + member = self._mlist.regular_members.get_member(self._anne) |
1147 | + self.assertIsNone(member) |
1148 | + # Create a new workflow with the previous workflow's save token, and |
1149 | + # restore its state. This models an approved subscription and should |
1150 | + # result in the user getting subscribed. |
1151 | + approved_workflow = SubscriptionWorkflow(self._mlist) |
1152 | + approved_workflow.token = workflow.token |
1153 | + approved_workflow.restore() |
1154 | + list(approved_workflow) |
1155 | + # Now the user is subscribed to the mailing list. |
1156 | + member = self._mlist.regular_members.get_member(self._anne) |
1157 | + self.assertEqual(member.address, anne) |
1158 | + |
1159 | + def test_get_moderator_approval_log_on_hold(self): |
1160 | + # When the subscription is held for moderator approval, a message is |
1161 | + # logged. |
1162 | + mark = LogFileMark('mailman.subscribe') |
1163 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
1164 | + anne = self._user_manager.create_address(self._anne) |
1165 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1166 | + pre_verified=True, |
1167 | + pre_confirmed=True) |
1168 | + # Consume the entire state machine. |
1169 | + list(workflow) |
1170 | + line = mark.readline() |
1171 | + self.assertEqual( |
1172 | + line[29:-1], |
1173 | + 'test@example.com: held subscription request from anne@example.com' |
1174 | + ) |
1175 | + |
1176 | + def test_get_moderator_approval_notifies_moderators(self): |
1177 | + # When the subscription is held for moderator approval, and the list |
1178 | + # is so configured, a notification is sent to the list moderators. |
1179 | + self._mlist.admin_immed_notify = True |
1180 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
1181 | + anne = self._user_manager.create_address(self._anne) |
1182 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1183 | + pre_verified=True, |
1184 | + pre_confirmed=True) |
1185 | + # Consume the entire state machine. |
1186 | + list(workflow) |
1187 | + items = get_queue_messages('virgin') |
1188 | + self.assertEqual(len(items), 1) |
1189 | + message = items[0].msg |
1190 | + self.assertEqual(message['From'], 'test-owner@example.com') |
1191 | + self.assertEqual(message['To'], 'test-owner@example.com') |
1192 | + self.assertEqual( |
1193 | + message['Subject'], |
1194 | + 'New subscription request to Test from anne@example.com') |
1195 | + self.assertEqual(message.get_payload(), """\ |
1196 | +Your authorization is required for a mailing list subscription request |
1197 | +approval: |
1198 | + |
1199 | + For: anne@example.com |
1200 | + List: test@example.com""") |
1201 | + |
1202 | + def test_get_moderator_approval_no_notifications(self): |
1203 | + # When the subscription is held for moderator approval, and the list |
1204 | + # is so configured, a notification is sent to the list moderators. |
1205 | + self._mlist.admin_immed_notify = False |
1206 | + self._mlist.subscription_policy = SubscriptionPolicy.moderate |
1207 | + anne = self._user_manager.create_address(self._anne) |
1208 | + workflow = SubscriptionWorkflow(self._mlist, anne, |
1209 | + pre_verified=True, |
1210 | + pre_confirmed=True) |
1211 | + # Consume the entire state machine. |
1212 | + list(workflow) |
1213 | + items = get_queue_messages('virgin') |
1214 | + self.assertEqual(len(items), 0) |
1215 | + |
1216 | + def test_send_confirmation(self): |
1217 | + # A confirmation message gets sent when the address is not verified. |
1218 | + anne = self._user_manager.create_address(self._anne) |
1219 | + self.assertIsNone(anne.verified_on) |
1220 | + # Run the workflow to model the confirmation step. |
1221 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
1222 | + list(workflow) |
1223 | + items = get_queue_messages('virgin') |
1224 | + self.assertEqual(len(items), 1) |
1225 | + message = items[0].msg |
1226 | + token = workflow.token |
1227 | + self.assertEqual(message['Subject'], 'confirm {}'.format(token)) |
1228 | + self.assertEqual( |
1229 | + message['From'], 'test-confirm+{}@example.com'.format(token)) |
1230 | + |
1231 | + def test_send_confirmation_pre_confirmed(self): |
1232 | + # A confirmation message gets sent when the address is not verified |
1233 | + # but the subscription is pre-confirmed. |
1234 | + anne = self._user_manager.create_address(self._anne) |
1235 | + self.assertIsNone(anne.verified_on) |
1236 | + # Run the workflow to model the confirmation step. |
1237 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_confirmed=True) |
1238 | + list(workflow) |
1239 | + items = get_queue_messages('virgin') |
1240 | + self.assertEqual(len(items), 1) |
1241 | + message = items[0].msg |
1242 | + token = workflow.token |
1243 | + self.assertEqual( |
1244 | + message['Subject'], 'confirm {}'.format(workflow.token)) |
1245 | + self.assertEqual( |
1246 | + message['From'], 'test-confirm+{}@example.com'.format(token)) |
1247 | + |
1248 | + def test_send_confirmation_pre_verified(self): |
1249 | + # A confirmation message gets sent even when the address is verified |
1250 | + # when the subscription must be confirmed. |
1251 | + self._mlist.subscription_policy = SubscriptionPolicy.confirm |
1252 | + anne = self._user_manager.create_address(self._anne) |
1253 | + self.assertIsNone(anne.verified_on) |
1254 | + # Run the workflow to model the confirmation step. |
1255 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
1256 | + list(workflow) |
1257 | + items = get_queue_messages('virgin') |
1258 | + self.assertEqual(len(items), 1) |
1259 | + message = items[0].msg |
1260 | + token = workflow.token |
1261 | + self.assertEqual( |
1262 | + message['Subject'], 'confirm {}'.format(workflow.token)) |
1263 | + self.assertEqual( |
1264 | + message['From'], 'test-confirm+{}@example.com'.format(token)) |
1265 | + |
1266 | + def test_do_confirm_verify_address(self): |
1267 | + # The address is not yet verified, nor are we pre-verifying. A |
1268 | + # confirmation message will be sent. When the user confirms their |
1269 | + # subscription request, the address will end up being verified. |
1270 | + anne = self._user_manager.create_address(self._anne) |
1271 | + self.assertIsNone(anne.verified_on) |
1272 | + # Run the workflow to model the confirmation step. |
1273 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
1274 | + list(workflow) |
1275 | + # The address is still not verified. |
1276 | + self.assertIsNone(anne.verified_on) |
1277 | + confirm_workflow = SubscriptionWorkflow(self._mlist) |
1278 | + confirm_workflow.token = workflow.token |
1279 | + confirm_workflow.restore() |
1280 | + confirm_workflow.run_thru('do_confirm_verify') |
1281 | + # The address is now verified. |
1282 | + self.assertIsNotNone(anne.verified_on) |
1283 | + |
1284 | + def test_do_confirmation_subscribes_user(self): |
1285 | + # Subscriptions to the mailing list must be confirmed. Once that's |
1286 | + # done, the user's address (which is not initially verified) gets |
1287 | + # subscribed to the mailing list. |
1288 | + self._mlist.subscription_policy = SubscriptionPolicy.confirm |
1289 | + anne = self._user_manager.create_address(self._anne) |
1290 | + self.assertIsNone(anne.verified_on) |
1291 | + workflow = SubscriptionWorkflow(self._mlist, anne) |
1292 | + list(workflow) |
1293 | + self.assertIsNone(self._mlist.regular_members.get_member(self._anne)) |
1294 | + confirm_workflow = SubscriptionWorkflow(self._mlist) |
1295 | + confirm_workflow.token = workflow.token |
1296 | + confirm_workflow.restore() |
1297 | + list(confirm_workflow) |
1298 | + self.assertIsNotNone(anne.verified_on) |
1299 | + self.assertEqual( |
1300 | + self._mlist.regular_members.get_member(self._anne).address, anne) |
1301 | + |
1302 | + def test_prevent_confirmation_replay_attacks(self): |
1303 | + # Ensure that if the workflow requires two confirmations, e.g. first |
1304 | + # the user confirming their subscription, and then the moderator |
1305 | + # approving it, that different tokens are used in these two cases. |
1306 | + self._mlist.subscription_policy = \ |
1307 | + SubscriptionPolicy.confirm_then_moderate |
1308 | + anne = self._user_manager.create_address(self._anne) |
1309 | + workflow = SubscriptionWorkflow(self._mlist, anne, pre_verified=True) |
1310 | + # Run the state machine up to the first confirmation, and cache the |
1311 | + # confirmation token. |
1312 | + list(workflow) |
1313 | + token = workflow.token |
1314 | + # Anne is not yet a member of the mailing list. |
1315 | + member = self._mlist.regular_members.get_member(self._anne) |
1316 | + self.assertIsNone(member) |
1317 | + # The old token will not work for moderator approval. |
1318 | + moderator_workflow = SubscriptionWorkflow(self._mlist) |
1319 | + moderator_workflow.token = token |
1320 | + moderator_workflow.restore() |
1321 | + list(moderator_workflow) |
1322 | + # While we wait for the moderator to approve the subscription, note |
1323 | + # that there's a new token for the next steps. |
1324 | + self.assertNotEqual(token, moderator_workflow.token) |
1325 | + # The old token won't work. |
1326 | + final_workflow = SubscriptionWorkflow(self._mlist) |
1327 | + final_workflow.token = token |
1328 | + self.assertRaises(LookupError, final_workflow.restore) |
1329 | + # Running this workflow will fail. |
1330 | + self.assertRaises(AssertionError, list, final_workflow) |
1331 | + # Anne is still not subscribed. |
1332 | + member = self._mlist.regular_members.get_member(self._anne) |
1333 | + self.assertIsNone(member) |
1334 | + # However, if we use the new token, her subscription request will be |
1335 | + # approved by the moderator. |
1336 | + final_workflow.token = moderator_workflow.token |
1337 | + final_workflow.restore() |
1338 | + list(final_workflow) |
1339 | + # And now Anne is a member. |
1340 | + member = self._mlist.regular_members.get_member(self._anne) |
1341 | + self.assertEqual(member.address.email, self._anne) |
1342 | |
1343 | === added file 'src/mailman/app/tests/test_workflow.py' |
1344 | --- src/mailman/app/tests/test_workflow.py 1970-01-01 00:00:00 +0000 |
1345 | +++ src/mailman/app/tests/test_workflow.py 2015-04-15 04:15:28 +0000 |
1346 | @@ -0,0 +1,128 @@ |
1347 | +# Copyright (C) 2015 by the Free Software Foundation, Inc. |
1348 | +# |
1349 | +# This file is part of GNU Mailman. |
1350 | +# |
1351 | +# GNU Mailman is free software: you can redistribute it and/or modify it under |
1352 | +# the terms of the GNU General Public License as published by the Free |
1353 | +# Software Foundation, either version 3 of the License, or (at your option) |
1354 | +# any later version. |
1355 | +# |
1356 | +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT |
1357 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
1358 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
1359 | +# more details. |
1360 | +# |
1361 | +# You should have received a copy of the GNU General Public License along with |
1362 | +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. |
1363 | + |
1364 | +"""App-level workflow tests.""" |
1365 | + |
1366 | +__all__ = [ |
1367 | + 'TestWorkflow', |
1368 | + ] |
1369 | + |
1370 | + |
1371 | +import unittest |
1372 | + |
1373 | +from mailman.app.workflow import Workflow |
1374 | +from mailman.testing.layers import ConfigLayer |
1375 | + |
1376 | + |
1377 | +class MyWorkflow(Workflow): |
1378 | + INITIAL_STATE = 'first' |
1379 | + SAVE_ATTRIBUTES = ('ant', 'bee', 'cat') |
1380 | + |
1381 | + def __init__(self): |
1382 | + super().__init__() |
1383 | + self.token = 'test-workflow' |
1384 | + self.ant = 1 |
1385 | + self.bee = 2 |
1386 | + self.cat = 3 |
1387 | + self.dog = 4 |
1388 | + |
1389 | + def _step_first(self): |
1390 | + self.push('second') |
1391 | + return 'one' |
1392 | + |
1393 | + def _step_second(self): |
1394 | + self.push('third') |
1395 | + return 'two' |
1396 | + |
1397 | + def _step_third(self): |
1398 | + return 'three' |
1399 | + |
1400 | + |
1401 | + |
1402 | |
1403 | +class TestWorkflow(unittest.TestCase): |
1404 | + layer = ConfigLayer |
1405 | + |
1406 | + def setUp(self): |
1407 | + self._workflow = iter(MyWorkflow()) |
1408 | + |
1409 | + def test_basic_workflow(self): |
1410 | + # The work flows from one state to the next. |
1411 | + results = list(self._workflow) |
1412 | + self.assertEqual(results, ['one', 'two', 'three']) |
1413 | + |
1414 | + def test_partial_workflow(self): |
1415 | + # You don't have to flow through every step. |
1416 | + results = next(self._workflow) |
1417 | + self.assertEqual(results, 'one') |
1418 | + |
1419 | + def test_exhaust_workflow(self): |
1420 | + # Manually flow through a few steps, then consume the whole thing. |
1421 | + results = [next(self._workflow)] |
1422 | + results.extend(self._workflow) |
1423 | + self.assertEqual(results, ['one', 'two', 'three']) |
1424 | + |
1425 | + def test_save_and_restore_workflow(self): |
1426 | + # Without running any steps, save and restore the workflow. Then |
1427 | + # consume the restored workflow. |
1428 | + self._workflow.save() |
1429 | + new_workflow = MyWorkflow() |
1430 | + new_workflow.restore() |
1431 | + results = list(new_workflow) |
1432 | + self.assertEqual(results, ['one', 'two', 'three']) |
1433 | + |
1434 | + def test_save_and_restore_partial_workflow(self): |
1435 | + # After running a few steps, save and restore the workflow. Then |
1436 | + # consume the restored workflow. |
1437 | + next(self._workflow) |
1438 | + self._workflow.save() |
1439 | + new_workflow = MyWorkflow() |
1440 | + new_workflow.restore() |
1441 | + results = list(new_workflow) |
1442 | + self.assertEqual(results, ['two', 'three']) |
1443 | + |
1444 | + def test_save_and_restore_exhausted_workflow(self): |
1445 | + # After consuming the entire workflow, save and restore it. |
1446 | + list(self._workflow) |
1447 | + self._workflow.save() |
1448 | + new_workflow = MyWorkflow() |
1449 | + new_workflow.restore() |
1450 | + results = list(new_workflow) |
1451 | + self.assertEqual(len(results), 0) |
1452 | + |
1453 | + def test_save_and_restore_attributes(self): |
1454 | + # Saved attributes are restored. |
1455 | + self._workflow.ant = 9 |
1456 | + self._workflow.bee = 8 |
1457 | + self._workflow.cat = 7 |
1458 | + # Don't save .dog. |
1459 | + self._workflow.save() |
1460 | + new_workflow = MyWorkflow() |
1461 | + new_workflow.restore() |
1462 | + self.assertEqual(new_workflow.ant, 9) |
1463 | + self.assertEqual(new_workflow.bee, 8) |
1464 | + self.assertEqual(new_workflow.cat, 7) |
1465 | + self.assertEqual(new_workflow.dog, 4) |
1466 | + |
1467 | + def test_run_thru(self): |
1468 | + # Run all steps through the given one. |
1469 | + results = self._workflow.run_thru('second') |
1470 | + self.assertEqual(results, ['one', 'two']) |
1471 | + |
1472 | + def test_run_until(self): |
1473 | + # Run until (but not including) the given step. |
1474 | + results = self._workflow.run_until('second') |
1475 | + self.assertEqual(results, ['one']) |
1476 | |
1477 | === added file 'src/mailman/app/workflow.py' |
1478 | --- src/mailman/app/workflow.py 1970-01-01 00:00:00 +0000 |
1479 | +++ src/mailman/app/workflow.py 2015-04-15 04:15:28 +0000 |
1480 | @@ -0,0 +1,156 @@ |
1481 | +# Copyright (C) 2015 by the Free Software Foundation, Inc. |
1482 | +# |
1483 | +# This file is part of GNU Mailman. |
1484 | +# |
1485 | +# GNU Mailman is free software: you can redistribute it and/or modify it under |
1486 | +# the terms of the GNU General Public License as published by the Free |
1487 | +# Software Foundation, either version 3 of the License, or (at your option) |
1488 | +# any later version. |
1489 | +# |
1490 | +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT |
1491 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
1492 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
1493 | +# more details. |
1494 | +# |
1495 | +# You should have received a copy of the GNU General Public License along with |
1496 | +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. |
1497 | + |
1498 | +"""Generic workflow.""" |
1499 | + |
1500 | +__all__ = [ |
1501 | + 'Workflow', |
1502 | + ] |
1503 | + |
1504 | + |
1505 | +import sys |
1506 | +import json |
1507 | +import logging |
1508 | + |
1509 | +from collections import deque |
1510 | +from mailman.interfaces.workflow import IWorkflowStateManager |
1511 | +from zope.component import getUtility |
1512 | + |
1513 | + |
1514 | +COMMASPACE = ', ' |
1515 | +log = logging.getLogger('mailman.error') |
1516 | + |
1517 | + |
1518 | + |
1519 | |
1520 | +class Workflow: |
1521 | + """Generic workflow.""" |
1522 | + |
1523 | + SAVE_ATTRIBUTES = () |
1524 | + INITIAL_STATE = None |
1525 | + |
1526 | + def __init__(self): |
1527 | + self.token = None |
1528 | + self._next = deque() |
1529 | + self.push(self.INITIAL_STATE) |
1530 | + self.debug = False |
1531 | + self._count = 0 |
1532 | + |
1533 | + @property |
1534 | + def name(self): |
1535 | + return self.__class__.__name__ |
1536 | + |
1537 | + def __iter__(self): |
1538 | + return self |
1539 | + |
1540 | + def push(self, step): |
1541 | + self._next.append(step) |
1542 | + |
1543 | + def _pop(self): |
1544 | + name = self._next.popleft() |
1545 | + step = getattr(self, '_step_{}'.format(name)) |
1546 | + self._count += 1 |
1547 | + if self.debug: |
1548 | + print('[{:02d}] -> {}'.format(self._count, name), file=sys.stderr) |
1549 | + return name, step |
1550 | + |
1551 | + def __next__(self): |
1552 | + try: |
1553 | + name, step = self._pop() |
1554 | + return step() |
1555 | + except IndexError: |
1556 | + raise StopIteration |
1557 | + except: |
1558 | + log.exception('deque: {}'.format(COMMASPACE.join(self._next))) |
1559 | + raise |
1560 | + |
1561 | + def run_thru(self, stop_after): |
1562 | + """Run the state machine through and including the given step. |
1563 | + |
1564 | + :param stop_after: Name of method, sans prefix to run the |
1565 | + state machine through. In other words, the state machine runs |
1566 | + until the named method completes. |
1567 | + """ |
1568 | + results = [] |
1569 | + while True: |
1570 | + try: |
1571 | + name, step = self._pop() |
1572 | + except (StopIteration, IndexError): |
1573 | + # We're done. |
1574 | + break |
1575 | + results.append(step()) |
1576 | + if name == stop_after: |
1577 | + break |
1578 | + return results |
1579 | + |
1580 | + def run_until(self, stop_before): |
1581 | + """Trun the state machine until (not including) the given step. |
1582 | + |
1583 | + :param stop_before: Name of method, sans prefix that the |
1584 | + state machine is run until the method is reached. Unlike |
1585 | + `run_thru()` the named method is not run. |
1586 | + """ |
1587 | + results = [] |
1588 | + while True: |
1589 | + try: |
1590 | + name, step = self._pop() |
1591 | + except (StopIteration, IndexError): |
1592 | + # We're done. |
1593 | + break |
1594 | + if name == stop_before: |
1595 | + # Stop executing, but not before we push the last state back |
1596 | + # onto the deque. Otherwise, resuming the state machine would |
1597 | + # skip this step. |
1598 | + self._next.appendleft(step) |
1599 | + break |
1600 | + results.append(step()) |
1601 | + return results |
1602 | + |
1603 | + def save(self): |
1604 | + assert self.token, 'Workflow token must be set' |
1605 | + state_manager = getUtility(IWorkflowStateManager) |
1606 | + data = {attr: getattr(self, attr) for attr in self.SAVE_ATTRIBUTES} |
1607 | + # Note: only the next step is saved, not the whole stack. This is not |
1608 | + # an issue in practice, since there's never more than a single step in |
1609 | + # the queue anyway. If we want to support more than a single step in |
1610 | + # the queue *and* want to support state saving/restoring, change this |
1611 | + # method and the restore() method. |
1612 | + if len(self._next) == 0: |
1613 | + step = None |
1614 | + elif len(self._next) == 1: |
1615 | + step = self._next[0] |
1616 | + else: |
1617 | + raise AssertionError( |
1618 | + "Can't save a workflow state with more than one step " |
1619 | + "in the queue") |
1620 | + state_manager.save( |
1621 | + self.__class__.__name__, |
1622 | + self.token, |
1623 | + step, |
1624 | + json.dumps(data)) |
1625 | + |
1626 | + def restore(self): |
1627 | + state_manager = getUtility(IWorkflowStateManager) |
1628 | + state = state_manager.restore(self.__class__.__name__, self.token) |
1629 | + if state is None: |
1630 | + # The token doesn't exist in the database. |
1631 | + raise LookupError(self.token) |
1632 | + self._next.clear() |
1633 | + if state.step: |
1634 | + self._next.append(state.step) |
1635 | + if state.data is not None: |
1636 | + for attr, value in json.loads(state.data).items(): |
1637 | + setattr(self, attr, value) |
1638 | |
1639 | === modified file 'src/mailman/commands/docs/membership.rst' |
1640 | --- src/mailman/commands/docs/membership.rst 2015-04-06 20:07:04 +0000 |
1641 | +++ src/mailman/commands/docs/membership.rst 2015-04-15 04:15:28 +0000 |
1642 | @@ -70,7 +70,7 @@ |
1643 | Joining the sender |
1644 | ------------------ |
1645 | |
1646 | -When the message has a From field, that address will be subscribed. |
1647 | +When the message has a ``From`` field, that address will be subscribed. |
1648 | |
1649 | >>> msg = message_from_string("""\ |
1650 | ... From: Anne Person <anne@example.com> |
1651 | @@ -85,13 +85,10 @@ |
1652 | Confirmation email sent to Anne Person <anne@example.com> |
1653 | <BLANKLINE> |
1654 | |
1655 | -Anne is not yet a member because she must confirm her subscription request |
1656 | -first. |
1657 | +Anne is not yet a member of the mailing list because she must confirm her |
1658 | +subscription request first. |
1659 | |
1660 | - >>> from mailman.interfaces.usermanager import IUserManager |
1661 | - >>> from zope.component import getUtility |
1662 | - >>> user_manager = getUtility(IUserManager) |
1663 | - >>> print(user_manager.get_user('anne@example.com')) |
1664 | + >>> print(mlist.members.get_member('anne@example.com')) |
1665 | None |
1666 | |
1667 | Mailman has sent her the confirmation message. |
1668 | @@ -118,10 +115,7 @@ |
1669 | <BLANKLINE> |
1670 | Before you can start using GNU Mailman at this site, you must first |
1671 | confirm that this is your email address. You can do this by replying to |
1672 | - this message, keeping the Subject header intact. Or you can visit this |
1673 | - web page |
1674 | - <BLANKLINE> |
1675 | - http://lists.example.com/confirm/... |
1676 | + this message, keeping the Subject header intact. |
1677 | <BLANKLINE> |
1678 | If you do not wish to register this email address simply disregard this |
1679 | message. If you think you are being maliciously subscribed to the list, or |
1680 | @@ -130,8 +124,7 @@ |
1681 | alpha-owner@example.com |
1682 | <BLANKLINE> |
1683 | |
1684 | -Once Anne confirms her registration, she will be made a member of the mailing |
1685 | -list. |
1686 | +Anne confirms her registration. |
1687 | :: |
1688 | |
1689 | >>> def extract_token(message): |
1690 | @@ -156,13 +149,7 @@ |
1691 | Confirmed |
1692 | <BLANKLINE> |
1693 | |
1694 | - >>> user = user_manager.get_user('anne@example.com') |
1695 | - >>> print(user.display_name) |
1696 | - Anne Person |
1697 | - >>> list(user.addresses) |
1698 | - [<Address: Anne Person <anne@example.com> [verified] at ...>] |
1699 | - |
1700 | -Anne is also now a member of the mailing list. |
1701 | +Anne is now a member of the mailing list. |
1702 | |
1703 | >>> mlist.members.get_member('anne@example.com') |
1704 | <Member: Anne Person <anne@example.com> |
1705 | @@ -180,12 +167,7 @@ |
1706 | >>> print(join.process(mlist_2, msg, {}, (), Results())) |
1707 | ContinueProcessing.yes |
1708 | |
1709 | -Anne of course, is still registered. |
1710 | - |
1711 | - >>> print(user_manager.get_user('anne@example.com')) |
1712 | - <User "Anne Person" (...) at ...> |
1713 | - |
1714 | -But she is not a member of the mailing list. |
1715 | +Anne is not a member of the mailing list. |
1716 | |
1717 | >>> print(mlist_2.members.get_member('anne@example.com')) |
1718 | None |
1719 | @@ -257,7 +239,9 @@ |
1720 | will do. |
1721 | :: |
1722 | |
1723 | - >>> anne = user_manager.get_user('anne@example.com') |
1724 | + >>> from mailman.interfaces.usermanager import IUserManager |
1725 | + >>> from zope.component import getUtility |
1726 | + >>> anne = getUtility(IUserManager).get_user('anne@example.com') |
1727 | >>> address = anne.register('anne.person@example.org') |
1728 | |
1729 | >>> results = Results() |
1730 | @@ -333,11 +317,6 @@ |
1731 | ... raise AssertionError('No confirmation message') |
1732 | >>> token = extract_token(item.msg) |
1733 | |
1734 | -Bart is still not a user. |
1735 | - |
1736 | - >>> print(user_manager.get_user('bart@example.com')) |
1737 | - None |
1738 | - |
1739 | Bart replies to the original message, specifically keeping the Subject header |
1740 | intact except for any prefix. Mailman matches the token and confirms Bart as |
1741 | a user of the system. |
1742 | @@ -360,12 +339,7 @@ |
1743 | Confirmed |
1744 | <BLANKLINE> |
1745 | |
1746 | -Now Bart is a user... |
1747 | - |
1748 | - >>> print(user_manager.get_user('bart@example.com')) |
1749 | - <User "Bart Person" (...) at ...> |
1750 | - |
1751 | -...and a member of the mailing list. |
1752 | +Now Bart is now a member of the mailing list. |
1753 | |
1754 | >>> print(mlist.members.get_member('bart@example.com')) |
1755 | <Member: Bart Person <bart@example.com> |
1756 | |
1757 | === modified file 'src/mailman/commands/eml_confirm.py' |
1758 | --- src/mailman/commands/eml_confirm.py 2015-01-05 01:22:39 +0000 |
1759 | +++ src/mailman/commands/eml_confirm.py 2015-04-15 04:15:28 +0000 |
1760 | @@ -25,7 +25,6 @@ |
1761 | from mailman.core.i18n import _ |
1762 | from mailman.interfaces.command import ContinueProcessing, IEmailCommand |
1763 | from mailman.interfaces.registrar import IRegistrar |
1764 | -from zope.component import getUtility |
1765 | from zope.interface import implementer |
1766 | |
1767 | |
1768 | @@ -53,7 +52,11 @@ |
1769 | return ContinueProcessing.yes |
1770 | tokens.add(token) |
1771 | results.confirms = tokens |
1772 | - succeeded = getUtility(IRegistrar).confirm(token) |
1773 | + try: |
1774 | + succeeded = (IRegistrar(mlist).confirm(token) is None) |
1775 | + except LookupError: |
1776 | + # The token must not exist in the database. |
1777 | + succeeded = False |
1778 | if succeeded: |
1779 | print(_('Confirmed'), file=results) |
1780 | return ContinueProcessing.yes |
1781 | |
1782 | === modified file 'src/mailman/commands/eml_membership.py' |
1783 | --- src/mailman/commands/eml_membership.py 2015-01-05 01:22:39 +0000 |
1784 | +++ src/mailman/commands/eml_membership.py 2015-04-15 04:15:28 +0000 |
1785 | @@ -37,6 +37,28 @@ |
1786 | |
1787 | |
1788 | |
1789 | |
1790 | +def match_subscriber(email, display_name): |
1791 | + # Return something matching the email which should be used as the |
1792 | + # subscriber by the IRegistrar interface. |
1793 | + manager = getUtility(IUserManager) |
1794 | + # Is there a user with a preferred address matching the email? |
1795 | + user = manager.get_user(email) |
1796 | + if user is not None: |
1797 | + preferred = user.preferred_address |
1798 | + if preferred is not None and preferred.email == email.lower(): |
1799 | + return user |
1800 | + # Is there an address matching the email? |
1801 | + address = manager.get_address(email) |
1802 | + if address is not None: |
1803 | + return address |
1804 | + # Make a new user and subscribe their first (and only) address. We can't |
1805 | + # make the first address their preferred address because it hasn't been |
1806 | + # verified yet. |
1807 | + user = manager.make_user(email, display_name) |
1808 | + return list(user.addresses)[0] |
1809 | + |
1810 | + |
1811 | + |
1812 | |
1813 | @implementer(IEmailCommand) |
1814 | class Join: |
1815 | """The email 'join' command.""" |
1816 | @@ -60,35 +82,35 @@ |
1817 | delivery_mode = self._parse_arguments(arguments, results) |
1818 | if delivery_mode is ContinueProcessing.no: |
1819 | return ContinueProcessing.no |
1820 | - display_name, address = parseaddr(msg['from']) |
1821 | + display_name, email = parseaddr(msg['from']) |
1822 | # Address could be None or the empty string. |
1823 | - if not address: |
1824 | - address = msg.sender |
1825 | - if not address: |
1826 | + if not email: |
1827 | + email = msg.sender |
1828 | + if not email: |
1829 | print(_('$self.name: No valid address found to subscribe'), |
1830 | file=results) |
1831 | return ContinueProcessing.no |
1832 | - if isinstance(address, bytes): |
1833 | - address = address.decode('ascii') |
1834 | + if isinstance(email, bytes): |
1835 | + email = email.decode('ascii') |
1836 | # Have we already seen one join request from this user during the |
1837 | # processing of this email? |
1838 | joins = getattr(results, 'joins', set()) |
1839 | - if address in joins: |
1840 | + if email in joins: |
1841 | # Do not register this join. |
1842 | return ContinueProcessing.yes |
1843 | - joins.add(address) |
1844 | + joins.add(email) |
1845 | results.joins = joins |
1846 | - person = formataddr((display_name, address)) |
1847 | + person = formataddr((display_name, email)) |
1848 | # Is this person already a member of the list? Search for all |
1849 | # matching memberships. |
1850 | members = getUtility(ISubscriptionService).find_members( |
1851 | - address, mlist.list_id, MemberRole.member) |
1852 | + email, mlist.list_id, MemberRole.member) |
1853 | if len(members) > 0: |
1854 | print(_('$person is already a member'), file=results) |
1855 | - else: |
1856 | - getUtility(IRegistrar).register(mlist, address, |
1857 | - display_name, delivery_mode) |
1858 | - print(_('Confirmation email sent to $person'), file=results) |
1859 | + return ContinueProcessing.yes |
1860 | + subscriber = match_subscriber(email, display_name) |
1861 | + IRegistrar(mlist).register(subscriber) |
1862 | + print(_('Confirmation email sent to $person'), file=results) |
1863 | return ContinueProcessing.yes |
1864 | |
1865 | def _parse_arguments(self, arguments, results): |
1866 | |
1867 | === modified file 'src/mailman/commands/tests/test_confirm.py' |
1868 | --- src/mailman/commands/tests/test_confirm.py 2015-01-05 01:22:39 +0000 |
1869 | +++ src/mailman/commands/tests/test_confirm.py 2015-04-15 04:15:28 +0000 |
1870 | @@ -29,8 +29,9 @@ |
1871 | from mailman.email.message import Message |
1872 | from mailman.interfaces.command import ContinueProcessing |
1873 | from mailman.interfaces.registrar import IRegistrar |
1874 | +from mailman.interfaces.usermanager import IUserManager |
1875 | from mailman.runners.command import Results |
1876 | -from mailman.testing.helpers import get_queue_messages, reset_the_world |
1877 | +from mailman.testing.helpers import get_queue_messages |
1878 | from mailman.testing.layers import ConfigLayer |
1879 | from zope.component import getUtility |
1880 | |
1881 | @@ -43,15 +44,13 @@ |
1882 | |
1883 | def setUp(self): |
1884 | self._mlist = create_list('test@example.com') |
1885 | - self._token = getUtility(IRegistrar).register( |
1886 | - self._mlist, 'anne@example.com', 'Anne Person') |
1887 | + anne = getUtility(IUserManager).create_address( |
1888 | + 'anne@example.com', 'Anne Person') |
1889 | + self._token = IRegistrar(self._mlist).register(anne) |
1890 | self._command = Confirm() |
1891 | # Clear the virgin queue. |
1892 | get_queue_messages('virgin') |
1893 | |
1894 | - def tearDown(self): |
1895 | - reset_the_world() |
1896 | - |
1897 | def test_welcome_message(self): |
1898 | # A confirmation causes a welcome message to be sent to the member, if |
1899 | # enabled by the mailing list. |
1900 | |
1901 | === modified file 'src/mailman/config/configure.zcml' |
1902 | --- src/mailman/config/configure.zcml 2014-09-23 12:58:38 +0000 |
1903 | +++ src/mailman/config/configure.zcml 2015-04-15 04:15:28 +0000 |
1904 | @@ -40,6 +40,12 @@ |
1905 | factory="mailman.model.requests.ListRequests" |
1906 | /> |
1907 | |
1908 | + <adapter |
1909 | + for="mailman.interfaces.mailinglist.IMailingList" |
1910 | + provides="mailman.interfaces.registrar.IRegistrar" |
1911 | + factory="mailman.app.registrar.Registrar" |
1912 | + /> |
1913 | + |
1914 | <utility |
1915 | provides="mailman.interfaces.bounce.IBounceProcessor" |
1916 | factory="mailman.model.bounce.BounceProcessor" |
1917 | @@ -88,11 +94,6 @@ |
1918 | /> |
1919 | |
1920 | <utility |
1921 | - provides="mailman.interfaces.registrar.IRegistrar" |
1922 | - factory="mailman.app.registrar.Registrar" |
1923 | - /> |
1924 | - |
1925 | - <utility |
1926 | provides="mailman.interfaces.styles.IStyleManager" |
1927 | factory="mailman.styles.manager.StyleManager" |
1928 | /> |
1929 | @@ -117,4 +118,9 @@ |
1930 | factory="mailman.app.templates.TemplateLoader" |
1931 | /> |
1932 | |
1933 | + <utility |
1934 | + provides="mailman.interfaces.workflow.IWorkflowStateManager" |
1935 | + factory="mailman.model.workflow.WorkflowStateManager" |
1936 | + /> |
1937 | + |
1938 | </configure> |
1939 | |
1940 | === added file 'src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py' |
1941 | --- src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py 1970-01-01 00:00:00 +0000 |
1942 | +++ src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py 2015-04-15 04:15:28 +0000 |
1943 | @@ -0,0 +1,41 @@ |
1944 | +"""List subscription policy |
1945 | + |
1946 | +Revision ID: 16c2b25c7b |
1947 | +Revises: 46e92facee7 |
1948 | +Create Date: 2015-03-21 11:00:44.634883 |
1949 | + |
1950 | +""" |
1951 | + |
1952 | +# revision identifiers, used by Alembic. |
1953 | +revision = '16c2b25c7b' |
1954 | +down_revision = '46e92facee7' |
1955 | + |
1956 | +from alembic import op |
1957 | +import sqlalchemy as sa |
1958 | + |
1959 | +from mailman.database.types import Enum |
1960 | +from mailman.interfaces.mailinglist import SubscriptionPolicy |
1961 | + |
1962 | + |
1963 | +def upgrade(): |
1964 | + |
1965 | + ### Update the schema |
1966 | + op.add_column('mailinglist', sa.Column( |
1967 | + 'subscription_policy', Enum(SubscriptionPolicy), nullable=True)) |
1968 | + |
1969 | + ### Now migrate the data |
1970 | + # don't import the table definition from the models, it may break this |
1971 | + # migration when the model is updated in the future (see the Alembic doc) |
1972 | + mlist = sa.sql.table('mailinglist', |
1973 | + sa.sql.column('subscription_policy', Enum(SubscriptionPolicy)) |
1974 | + ) |
1975 | + # there were no enforced subscription policy before, so all lists are |
1976 | + # considered open |
1977 | + op.execute(mlist.update().values( |
1978 | + {'subscription_policy': op.inline_literal(SubscriptionPolicy.open)})) |
1979 | + |
1980 | + |
1981 | +def downgrade(): |
1982 | + if op.get_bind().dialect.name != 'sqlite': |
1983 | + # SQLite does not support dropping columns. |
1984 | + op.drop_column('mailinglist', 'subscription_policy') |
1985 | |
1986 | === added file 'src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py' |
1987 | --- src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py 1970-01-01 00:00:00 +0000 |
1988 | +++ src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py 2015-04-15 04:15:28 +0000 |
1989 | @@ -0,0 +1,28 @@ |
1990 | +"""Workflow state table |
1991 | + |
1992 | +Revision ID: 2bb9b382198 |
1993 | +Revises: 16c2b25c7b |
1994 | +Create Date: 2015-03-25 18:09:18.338790 |
1995 | + |
1996 | +""" |
1997 | + |
1998 | +# revision identifiers, used by Alembic. |
1999 | +revision = '2bb9b382198' |
2000 | +down_revision = '16c2b25c7b' |
2001 | + |
2002 | +from alembic import op |
2003 | +import sqlalchemy as sa |
2004 | + |
2005 | + |
2006 | +def upgrade(): |
2007 | + op.create_table('workflowstate', |
2008 | + sa.Column('name', sa.Unicode(), nullable=False), |
2009 | + sa.Column('key', sa.Unicode(), nullable=False), |
2010 | + sa.Column('step', sa.Unicode(), nullable=True), |
2011 | + sa.Column('data', sa.Unicode(), nullable=True), |
2012 | + sa.PrimaryKeyConstraint('name', 'key') |
2013 | + ) |
2014 | + |
2015 | + |
2016 | +def downgrade(): |
2017 | + op.drop_table('workflowstate') |
2018 | |
2019 | === modified file 'src/mailman/database/transaction.py' |
2020 | --- src/mailman/database/transaction.py 2015-02-13 08:13:06 +0000 |
2021 | +++ src/mailman/database/transaction.py 2015-04-15 04:15:28 +0000 |
2022 | @@ -19,6 +19,7 @@ |
2023 | |
2024 | __all__ = [ |
2025 | 'dbconnection', |
2026 | + 'flush', |
2027 | 'transaction', |
2028 | 'transactional', |
2029 | ] |
2030 | @@ -63,6 +64,22 @@ |
2031 | |
2032 | |
2033 | |
2034 | |
2035 | +@contextmanager |
2036 | +def flush(): |
2037 | + """Context manager for flushing SQLAlchemy. |
2038 | + |
2039 | + We need this for SA whereas we didn't need it for Storm because the latter |
2040 | + did auto-reloads. However, in SA this is needed when we add or delete |
2041 | + objects from the database. Use it when you need the id after adding, or |
2042 | + when you want to be sure the object won't be found after a delete. |
2043 | + |
2044 | + This is lighter weight than committing the transaction. |
2045 | + """ |
2046 | + yield |
2047 | + config.db.store.flush() |
2048 | + |
2049 | + |
2050 | + |
2051 | |
2052 | def dbconnection(function): |
2053 | """Decorator for getting at the database connection. |
2054 | |
2055 | |
2056 | === modified file 'src/mailman/interfaces/mailinglist.py' |
2057 | --- src/mailman/interfaces/mailinglist.py 2015-01-05 01:22:39 +0000 |
2058 | +++ src/mailman/interfaces/mailinglist.py 2015-04-15 04:15:28 +0000 |
2059 | @@ -25,6 +25,7 @@ |
2060 | 'IMailingList', |
2061 | 'Personalization', |
2062 | 'ReplyToMunging', |
2063 | + 'SubscriptionPolicy', |
2064 | ] |
2065 | |
2066 | |
2067 | @@ -53,6 +54,18 @@ |
2068 | explicit_header = 2 |
2069 | |
2070 | |
2071 | +class SubscriptionPolicy(Enum): |
2072 | + # Neither confirmation, nor moderator approval is required. |
2073 | + open = 0 |
2074 | + # The user must confirm the subscription. |
2075 | + confirm = 1 |
2076 | + # The moderator must approve the subscription. |
2077 | + moderate = 2 |
2078 | + # The user must first confirm their subscription, and then if that is |
2079 | + # successful, the moderator must also approve it. |
2080 | + confirm_then_moderate = 3 |
2081 | + |
2082 | + |
2083 | |
2084 | |
2085 | class IMailingList(Interface): |
2086 | """A mailing list.""" |
2087 | @@ -234,6 +247,9 @@ |
2088 | deliver disabled or not, or of the type of digest they are to |
2089 | receive.""") |
2090 | |
2091 | + subscription_policy = Attribute( |
2092 | + """The policy for subscribing new members to the list.""") |
2093 | + |
2094 | subscribers = Attribute( |
2095 | """An iterator over all IMembers subscribed to this list, with any |
2096 | role. |
2097 | |
2098 | === modified file 'src/mailman/interfaces/member.py' |
2099 | --- src/mailman/interfaces/member.py 2015-01-05 01:22:39 +0000 |
2100 | +++ src/mailman/interfaces/member.py 2015-04-15 04:15:28 +0000 |
2101 | @@ -123,7 +123,7 @@ |
2102 | """The address is not allowed to subscribe to the mailing list.""" |
2103 | |
2104 | def __init__(self, mlist, address): |
2105 | - super(MembershipIsBannedError, self).__init__() |
2106 | + super().__init__() |
2107 | self._mlist = mlist |
2108 | self._address = address |
2109 | |
2110 | @@ -175,6 +175,14 @@ |
2111 | user = Attribute( |
2112 | """The user associated with this member.""") |
2113 | |
2114 | + subscriber = Attribute( |
2115 | + """The object representing how this member is subscribed. |
2116 | + |
2117 | + This will be an ``IAddress`` if the user is subscribed via an explicit |
2118 | + address, otherwise if the the user is subscribed via their preferred |
2119 | + address, it will be an ``IUser``. |
2120 | + """) |
2121 | + |
2122 | preferences = Attribute( |
2123 | """This member's preferences.""") |
2124 | |
2125 | |
2126 | === modified file 'src/mailman/interfaces/pending.py' |
2127 | --- src/mailman/interfaces/pending.py 2015-01-05 01:22:39 +0000 |
2128 | +++ src/mailman/interfaces/pending.py 2015-04-15 04:15:28 +0000 |
2129 | @@ -82,11 +82,11 @@ |
2130 | :return: A token string for inclusion in urls and email confirmations. |
2131 | """ |
2132 | |
2133 | - def confirm(token, expunge=True): |
2134 | + def confirm(token, *, expunge=True): |
2135 | """Return the IPendable matching the token. |
2136 | |
2137 | :param token: The token string for the IPendable given by the `.add()` |
2138 | - method. |
2139 | + method, or None if there is no record associated with the token. |
2140 | :param expunge: A flag indicating whether the pendable record should |
2141 | also be removed from the database or not. |
2142 | :return: The matching IPendable or None if no match was found. |
2143 | @@ -94,3 +94,6 @@ |
2144 | |
2145 | def evict(): |
2146 | """Remove all pended items whose lifetime has expired.""" |
2147 | + |
2148 | + def count(): |
2149 | + """The number of pendables in the pendings database.""" |
2150 | |
2151 | === modified file 'src/mailman/interfaces/registrar.py' |
2152 | --- src/mailman/interfaces/registrar.py 2015-01-05 01:22:39 +0000 |
2153 | +++ src/mailman/interfaces/registrar.py 2015-04-15 04:15:28 +0000 |
2154 | @@ -35,79 +35,75 @@ |
2155 | class ConfirmationNeededEvent: |
2156 | """Triggered when an address needs confirmation. |
2157 | |
2158 | - Addresses must be verified before they can receive messages or post to |
2159 | - mailing list. When an address is registered with Mailman, via the |
2160 | - `IRegistrar` interface, an `IPendable` is created which represents the |
2161 | - pending registration. This pending registration is stored in the |
2162 | - database, keyed by a token. Then this event is triggered. |
2163 | - |
2164 | - There may be several ways to confirm an email address. On some sites, |
2165 | - registration may immediately produce a verification, e.g. because it is on |
2166 | - a known intranet. Or verification may occur via external database lookup |
2167 | - (e.g. LDAP). On most public mailing lists, a mail-back confirmation is |
2168 | - sent to the address, and only if they reply to the mail-back, or click on |
2169 | - an embedded link, is the registered address confirmed. |
2170 | + Addresses must be verified before they can receive messages or post |
2171 | + to mailing list. The confirmation message is sent to the user when |
2172 | + this event is triggered. |
2173 | """ |
2174 | - def __init__(self, mlist, pendable, token): |
2175 | + def __init__(self, mlist, token, email): |
2176 | self.mlist = mlist |
2177 | - self.pendable = pendable |
2178 | self.token = token |
2179 | + self.email = email |
2180 | |
2181 | |
2182 | |
2183 | |
2184 | class IRegistrar(Interface): |
2185 | - """Interface for registering and verifying email addresses and users. |
2186 | + """Interface for subscribing addresses and users. |
2187 | |
2188 | This is a higher level interface to user registration, email address |
2189 | confirmation, etc. than the IUserManager. The latter does no validation, |
2190 | syntax checking, or confirmation, while this interface does. |
2191 | """ |
2192 | |
2193 | - def register(mlist, email, display_name=None, delivery_mode=None): |
2194 | - """Register the email address, requesting verification. |
2195 | - |
2196 | - No `IAddress` or `IUser` is created during this step, but after |
2197 | - successful confirmation, it is guaranteed that an `IAddress` with a |
2198 | - linked `IUser` will exist. When a verified `IAddress` matching |
2199 | - `email` already exists, this method will do nothing, except link a new |
2200 | - `IUser` to the `IAddress` if one is not yet associated with the |
2201 | - email address. |
2202 | - |
2203 | - In all cases, the email address is sanity checked for validity first. |
2204 | - |
2205 | - :param mlist: The mailing list that is the focus of this registration. |
2206 | + def register(mlist, subscriber=None, *, |
2207 | + pre_verified=False, pre_confirmed=False, pre_approved=False): |
2208 | + """Subscribe an address or user according to subscription policies. |
2209 | + |
2210 | + The mailing list's subscription policy is used to subscribe |
2211 | + `subscriber` to the given mailing list. The subscriber can be |
2212 | + an ``IUser``, in which case the user must have a preferred |
2213 | + address, and that preferred address will be subscribed. The |
2214 | + subscriber can also be an ``IAddress``, in which case the |
2215 | + address will be subscribed. |
2216 | + |
2217 | + The workflow may pause (i.e. be serialized, saved, and |
2218 | + suspended) when some out-of-band confirmation step is required. |
2219 | + For example, if the user must confirm, or the moderator must |
2220 | + approve the subscription. Use the ``confirm(token)`` method to |
2221 | + resume the workflow. |
2222 | + |
2223 | + :param mlist: The mailing list to subscribe to. |
2224 | :type mlist: `IMailingList` |
2225 | - :param email: The email address to register. |
2226 | - :type email: str |
2227 | - :param display_name: The optional display name of the user. |
2228 | - :type display_name: str |
2229 | - :param delivery_mode: The optional delivery mode for this |
2230 | - registration. If not given, regular delivery is used. |
2231 | - :type delivery_mode: `DeliveryMode` |
2232 | - :return: The confirmation token string. |
2233 | - :rtype: str |
2234 | - :raises InvalidEmailAddressError: if the address is not allowed. |
2235 | + :param subscriber: The user or address to subscribe. |
2236 | + :type email: ``IUser`` or ``IAddress`` |
2237 | + :return: The confirmation token string, or None if the workflow |
2238 | + completes (i.e. the member has been subscribed). |
2239 | + :rtype: str or None |
2240 | + :raises MembershipIsBannedError: when the address being subscribed |
2241 | + appears in the global or list-centric bans. |
2242 | """ |
2243 | |
2244 | def confirm(token): |
2245 | - """Confirm the pending registration matched to the given `token`. |
2246 | - |
2247 | - Confirmation ensures that the IAddress exists and is linked to an |
2248 | - IUser, with the latter being created and linked if necessary. |
2249 | - |
2250 | - :param token: A token matching a pending event with a type of |
2251 | - 'registration'. |
2252 | - :return: Boolean indicating whether the confirmation succeeded or |
2253 | - not. It may fail if the token is no longer in the database, or if |
2254 | - the token did not match a registration event. |
2255 | + """Continue any paused workflow. |
2256 | + |
2257 | + Confirmation may occur after the user confirms their |
2258 | + subscription request, or their email address must be verified, |
2259 | + or the moderator must approve the subscription request. |
2260 | + |
2261 | + :param token: A token matching a workflow. |
2262 | + :type token: string |
2263 | + :return: The new token for any follow up confirmation, or None if the |
2264 | + user was subscribed. |
2265 | + :rtype: str or None |
2266 | + :raises LookupError: when no workflow is associated with the token. |
2267 | """ |
2268 | |
2269 | def discard(token): |
2270 | - """Discard the pending registration matched to the given `token`. |
2271 | - |
2272 | - The event record is discarded and the IAddress is not verified. No |
2273 | - IUser is created. |
2274 | + """Discard the workflow matched to the given `token`. |
2275 | |
2276 | :param token: A token matching a pending event with a type of |
2277 | 'registration'. |
2278 | + :raises LookupError: when no workflow is associated with the token. |
2279 | """ |
2280 | + |
2281 | + def evict(): |
2282 | + """Evict all saved workflows which have expired.""" |
2283 | |
2284 | === modified file 'src/mailman/interfaces/roster.py' |
2285 | --- src/mailman/interfaces/roster.py 2015-01-05 01:22:39 +0000 |
2286 | +++ src/mailman/interfaces/roster.py 2015-04-15 04:15:28 +0000 |
2287 | @@ -53,11 +53,26 @@ |
2288 | managed by this roster. |
2289 | """) |
2290 | |
2291 | - def get_member(address): |
2292 | + def get_member(email): |
2293 | """Get the member for the given address. |
2294 | |
2295 | - :param address: The email address to search for. |
2296 | - :type address: text |
2297 | + *Note* that it is possible for an email to be subscribed to a |
2298 | + mailing list twice, once through its explicit address and once |
2299 | + indirectly through a user's preferred address. In this case, |
2300 | + this API always returns the explicit address. Use |
2301 | + ``get_memberships()`` to return them all. |
2302 | + |
2303 | + :param email: The email address to search for. |
2304 | + :type email: string |
2305 | :return: The member if found, otherwise None |
2306 | :rtype: `IMember` or None |
2307 | """ |
2308 | + |
2309 | + def get_memberships(email): |
2310 | + """Get the memberships for the given address. |
2311 | + |
2312 | + :param email: The email address to search for. |
2313 | + :type email: string |
2314 | + :return: All the memberships associated with this email address. |
2315 | + :rtype: sequence of length 0, 1, or 2 of ``IMember`` |
2316 | + """ |
2317 | |
2318 | === added file 'src/mailman/interfaces/workflow.py' |
2319 | --- src/mailman/interfaces/workflow.py 1970-01-01 00:00:00 +0000 |
2320 | +++ src/mailman/interfaces/workflow.py 2015-04-15 04:15:28 +0000 |
2321 | @@ -0,0 +1,80 @@ |
2322 | +# Copyright (C) 2015 by the Free Software Foundation, Inc. |
2323 | +# |
2324 | +# This file is part of GNU Mailman. |
2325 | +# |
2326 | +# GNU Mailman is free software: you can redistribute it and/or modify it under |
2327 | +# the terms of the GNU General Public License as published by the Free |
2328 | +# Software Foundation, either version 3 of the License, or (at your option) |
2329 | +# any later version. |
2330 | +# |
2331 | +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT |
2332 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
2333 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
2334 | +# more details. |
2335 | +# |
2336 | +# You should have received a copy of the GNU General Public License along with |
2337 | +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. |
2338 | + |
2339 | +"""Interfaces describing the state of a workflow.""" |
2340 | + |
2341 | +__all__ = [ |
2342 | + 'IWorkflowState', |
2343 | + 'IWorkflowStateManager', |
2344 | + ] |
2345 | + |
2346 | + |
2347 | +from zope.interface import Attribute, Interface |
2348 | + |
2349 | + |
2350 | + |
2351 | |
2352 | +class IWorkflowState(Interface): |
2353 | + """The state of a workflow.""" |
2354 | + |
2355 | + name = Attribute('The name of the workflow.') |
2356 | + |
2357 | + token = Attribute('A unique key identifying the workflow instance.') |
2358 | + |
2359 | + step = Attribute("This workflow's next step.") |
2360 | + |
2361 | + data = Attribute('Additional data (may be JSON-encoded).') |
2362 | + |
2363 | + |
2364 | + |
2365 | |
2366 | +class IWorkflowStateManager(Interface): |
2367 | + """The workflow states manager.""" |
2368 | + |
2369 | + def save(name, token, step, data=None): |
2370 | + """Save the state of a workflow. |
2371 | + |
2372 | + :param name: The name of the workflow. |
2373 | + :type name: str |
2374 | + :param token: A unique token identifying this workflow instance. |
2375 | + :type token: str |
2376 | + :param step: The next step for this workflow. |
2377 | + :type step: str |
2378 | + :param data: Additional data (workflow-specific). |
2379 | + :type data: str |
2380 | + """ |
2381 | + |
2382 | + def restore(name, token): |
2383 | + """Get the saved state for a workflow or None if nothing was saved. |
2384 | + |
2385 | + :param name: The name of the workflow. |
2386 | + :type name: str |
2387 | + :param token: A unique token identifying this workflow instance. |
2388 | + :type token: str |
2389 | + :return: The saved state associated with this name/token pair, or None |
2390 | + if the pair isn't in the database. |
2391 | + :rtype: ``IWorkflowState`` |
2392 | + """ |
2393 | + |
2394 | + def discard(name, token): |
2395 | + """Throw away the saved state for a workflow. |
2396 | + |
2397 | + :param name: The name of the workflow. |
2398 | + :type name: str |
2399 | + :param token: A unique token identifying this workflow instance. |
2400 | + :type token: str |
2401 | + """ |
2402 | + |
2403 | + count = Attribute('The number of saved workflows in the database.') |
2404 | |
2405 | === modified file 'src/mailman/model/docs/membership.rst' |
2406 | --- src/mailman/model/docs/membership.rst 2014-12-13 20:12:54 +0000 |
2407 | +++ src/mailman/model/docs/membership.rst 2015-04-15 04:15:28 +0000 |
2408 | @@ -228,6 +228,38 @@ |
2409 | fperson@example.com MemberRole.nonmember |
2410 | |
2411 | |
2412 | +Subscriber type |
2413 | +=============== |
2414 | + |
2415 | +Members can be subscribed to a mailing list either via an explicit address, or |
2416 | +indirectly through a user's preferred address. Sometimes you want to know |
2417 | +which one it is. |
2418 | + |
2419 | +Herb subscribes to the mailing list via an explicit address. |
2420 | + |
2421 | + >>> herb = user_manager.create_address( |
2422 | + ... 'hperson@example.com', 'Herb Person') |
2423 | + >>> herb_member = mlist.subscribe(herb) |
2424 | + |
2425 | +Iris subscribes to the mailing list via her preferred address. |
2426 | + |
2427 | + >>> iris = user_manager.make_user( |
2428 | + ... 'iperson@example.com', 'Iris Person') |
2429 | + >>> preferred = list(iris.addresses)[0] |
2430 | + >>> from mailman.utilities.datetime import now |
2431 | + >>> preferred.verified_on = now() |
2432 | + >>> iris.preferred_address = preferred |
2433 | + >>> iris_member = mlist.subscribe(iris) |
2434 | + |
2435 | +When we need to know which way a member is subscribed, we can look at the this |
2436 | +attribute. |
2437 | + |
2438 | + >>> herb_member.subscriber |
2439 | + <Address: Herb Person <hperson@example.com> [not verified] at ...> |
2440 | + >>> iris_member.subscriber |
2441 | + <User "Iris Person" (5) at ...> |
2442 | + |
2443 | + |
2444 | Moderation actions |
2445 | ================== |
2446 | |
2447 | @@ -250,6 +282,8 @@ |
2448 | aperson@example.com MemberRole.member Action.defer |
2449 | bperson@example.com MemberRole.member Action.defer |
2450 | cperson@example.com MemberRole.member Action.defer |
2451 | + hperson@example.com MemberRole.member Action.defer |
2452 | + iperson@example.com MemberRole.member Action.defer |
2453 | |
2454 | Postings by nonmembers are held for moderator approval by default. |
2455 | |
2456 | @@ -272,7 +306,7 @@ |
2457 | >>> gwen_member = bee.subscribe(gwen_address) |
2458 | >>> for m in bee.members.members: |
2459 | ... print(m.member_id.int, m.mailing_list.list_id, m.address.email) |
2460 | - 7 bee.example.com gwen@example.com |
2461 | + 9 bee.example.com gwen@example.com |
2462 | |
2463 | Gwen gets a email address. |
2464 | |
2465 | @@ -288,7 +322,7 @@ |
2466 | |
2467 | >>> for m in bee.members.members: |
2468 | ... print(m.member_id.int, m.mailing_list.list_id, m.address.email) |
2469 | - 7 bee.example.com gperson@example.com |
2470 | + 9 bee.example.com gperson@example.com |
2471 | |
2472 | |
2473 | Events |
2474 | |
2475 | === modified file 'src/mailman/model/docs/pending.rst' |
2476 | --- src/mailman/model/docs/pending.rst 2014-12-14 19:11:28 +0000 |
2477 | +++ src/mailman/model/docs/pending.rst 2015-04-15 04:15:28 +0000 |
2478 | @@ -13,6 +13,11 @@ |
2479 | >>> from zope.component import getUtility |
2480 | >>> pendingdb = getUtility(IPendings) |
2481 | |
2482 | +There are nothing in the pendings database. |
2483 | + |
2484 | + >>> pendingdb.count() |
2485 | + 0 |
2486 | + |
2487 | The pending database can add any ``IPendable`` to the database, returning a |
2488 | token that can be used in urls and such. |
2489 | :: |
2490 | @@ -33,6 +38,11 @@ |
2491 | >>> len(token) |
2492 | 40 |
2493 | |
2494 | +There's exactly one entry in the pendings database now. |
2495 | + |
2496 | + >>> pendingdb.count() |
2497 | + 1 |
2498 | + |
2499 | There's not much you can do with tokens except to *confirm* them, which |
2500 | basically means returning the `IPendable` structure (as a dictionary) from the |
2501 | database that matches the token. If the token isn't in the database, None is |
2502 | |
2503 | === modified file 'src/mailman/model/docs/registration.rst' |
2504 | --- src/mailman/model/docs/registration.rst 2015-04-06 20:07:04 +0000 |
2505 | +++ src/mailman/model/docs/registration.rst 2015-04-15 04:15:28 +0000 |
2506 | @@ -1,333 +1,90 @@ |
2507 | -==================== |
2508 | -Address registration |
2509 | -==================== |
2510 | - |
2511 | -Before users can join a mailing list, they must first register with Mailman. |
2512 | -The only thing they must supply is an email address, although there is |
2513 | -additional information they may supply. All registered email addresses must |
2514 | -be verified before Mailman will send them any list traffic. |
2515 | - |
2516 | -The ``IUserManager`` manages users, but it does so at a fairly low level. |
2517 | -Specifically, it does not handle verification, email address syntax validity |
2518 | -checks, etc. The ``IRegistrar`` is the interface to the object handling all |
2519 | -this stuff. |
2520 | +============ |
2521 | +Registration |
2522 | +============ |
2523 | + |
2524 | +When a user wants to join a mailing list, they must register and verify their |
2525 | +email address. Then depending on how the mailing list is configured, they may |
2526 | +need to confirm their subscription and have it approved by the list |
2527 | +moderator. The ``IRegistrar`` interface manages this work flow. |
2528 | |
2529 | >>> from mailman.interfaces.registrar import IRegistrar |
2530 | - >>> from zope.component import getUtility |
2531 | - >>> registrar = getUtility(IRegistrar) |
2532 | - |
2533 | -Here is a helper function to check the token strings. |
2534 | - |
2535 | - >>> def check_token(token): |
2536 | - ... assert isinstance(token, str), 'Not a string' |
2537 | - ... assert len(token) == 40, 'Unexpected length: %d' % len(token) |
2538 | - ... assert token.isalnum(), 'Not alphanumeric' |
2539 | - ... print('ok') |
2540 | - |
2541 | -Here is a helper function to extract tokens from confirmation messages. |
2542 | - |
2543 | - >>> import re |
2544 | - >>> cre = re.compile('http://lists.example.com/confirm/(.*)') |
2545 | - >>> def extract_token(msg): |
2546 | - ... mo = cre.search(msg.get_payload()) |
2547 | - ... return mo.group(1) |
2548 | - |
2549 | - |
2550 | -Invalid email addresses |
2551 | -======================= |
2552 | - |
2553 | -Addresses are registered within the context of a mailing list, mostly so that |
2554 | -confirmation emails can come from some place. You also need the email |
2555 | -address of the user who is registering. |
2556 | - |
2557 | - >>> mlist = create_list('alpha@example.com') |
2558 | + |
2559 | +Registrars adapt mailing lists. |
2560 | + |
2561 | + >>> from mailman.interfaces.mailinglist import SubscriptionPolicy |
2562 | + >>> mlist = create_list('ant@example.com') |
2563 | >>> mlist.send_welcome_message = False |
2564 | - |
2565 | -Some amount of sanity checks are performed on the email address, although |
2566 | -honestly, not as much as probably should be done. Still, some patently bad |
2567 | -addresses are rejected outright. |
2568 | + >>> mlist.subscription_policy = SubscriptionPolicy.open |
2569 | + >>> registrar = IRegistrar(mlist) |
2570 | + |
2571 | +Usually, addresses are registered, but users with preferred addresses can be |
2572 | +registered too. |
2573 | + |
2574 | + >>> from mailman.interfaces.usermanager import IUserManager |
2575 | + >>> from zope.component import getUtility |
2576 | + >>> anne = getUtility(IUserManager).create_address( |
2577 | + ... 'anne@example.com', 'Anne Person') |
2578 | |
2579 | |
2580 | Register an email address |
2581 | ========================= |
2582 | |
2583 | -Registration of an unknown address creates nothing until the confirmation step |
2584 | -is complete. No ``IUser`` or ``IAddress`` is created at registration time, |
2585 | -but a record is added to the pending database, and the token for that record |
2586 | -is returned. |
2587 | - |
2588 | - >>> token = registrar.register(mlist, 'aperson@example.com', 'Anne Person') |
2589 | - >>> check_token(token) |
2590 | - ok |
2591 | - |
2592 | -There should be no records in the user manager for this address yet. |
2593 | - |
2594 | - >>> from mailman.interfaces.usermanager import IUserManager |
2595 | - >>> from zope.component import getUtility |
2596 | - >>> user_manager = getUtility(IUserManager) |
2597 | - >>> print(user_manager.get_user('aperson@example.com')) |
2598 | - None |
2599 | - >>> print(user_manager.get_address('aperson@example.com')) |
2600 | - None |
2601 | - |
2602 | -But this address is waiting for confirmation. |
2603 | - |
2604 | - >>> from mailman.interfaces.pending import IPendings |
2605 | - >>> pendingdb = getUtility(IPendings) |
2606 | - |
2607 | - >>> dump_msgdata(pendingdb.confirm(token, expunge=False)) |
2608 | - delivery_mode: regular |
2609 | - display_name : Anne Person |
2610 | - email : aperson@example.com |
2611 | - list_id : alpha.example.com |
2612 | - type : registration |
2613 | - |
2614 | - |
2615 | -Verification by email |
2616 | -===================== |
2617 | - |
2618 | -There is also a verification email sitting in the virgin queue now. This |
2619 | -message is sent to the user in order to verify the registered address. |
2620 | - |
2621 | - >>> from mailman.testing.helpers import get_queue_messages |
2622 | - >>> items = get_queue_messages('virgin') |
2623 | - >>> len(items) |
2624 | - 1 |
2625 | - >>> print(items[0].msg.as_string()) |
2626 | - MIME-Version: 1.0 |
2627 | - ... |
2628 | - Subject: confirm ... |
2629 | - From: alpha-confirm+...@example.com |
2630 | - To: aperson@example.com |
2631 | - ... |
2632 | - <BLANKLINE> |
2633 | - Email Address Registration Confirmation |
2634 | - <BLANKLINE> |
2635 | - Hello, this is the GNU Mailman server at example.com. |
2636 | - <BLANKLINE> |
2637 | - We have received a registration request for the email address |
2638 | - <BLANKLINE> |
2639 | - aperson@example.com |
2640 | - <BLANKLINE> |
2641 | - Before you can start using GNU Mailman at this site, you must first |
2642 | - confirm that this is your email address. You can do this by replying to |
2643 | - this message, keeping the Subject header intact. Or you can visit this |
2644 | - web page |
2645 | - <BLANKLINE> |
2646 | - http://lists.example.com/confirm/... |
2647 | - <BLANKLINE> |
2648 | - If you do not wish to register this email address simply disregard this |
2649 | - message. If you think you are being maliciously subscribed to the list, |
2650 | - or have any other questions, you may contact |
2651 | - <BLANKLINE> |
2652 | - alpha-owner@example.com |
2653 | - <BLANKLINE> |
2654 | - >>> dump_msgdata(items[0].msgdata) |
2655 | - _parsemsg : False |
2656 | - listid : alpha.example.com |
2657 | - nodecorate : True |
2658 | - recipients : {'aperson@example.com'} |
2659 | - reduced_list_headers: True |
2660 | - version : 3 |
2661 | - |
2662 | -The confirmation token shows up in several places, each of which provides an |
2663 | -easy way for the user to complete the confirmation. The token will always |
2664 | -appear in a URL in the body of the message. |
2665 | - |
2666 | - >>> sent_token = extract_token(items[0].msg) |
2667 | - >>> sent_token == token |
2668 | - True |
2669 | - |
2670 | -The same token will appear in the ``From`` header. |
2671 | - |
2672 | - >>> items[0].msg['from'] == 'alpha-confirm+' + token + '@example.com' |
2673 | - True |
2674 | - |
2675 | -It will also appear in the ``Subject`` header. |
2676 | - |
2677 | - >>> items[0].msg['subject'] == 'confirm ' + token |
2678 | - True |
2679 | - |
2680 | -The user would then validate their registered address by clicking on a url or |
2681 | -responding to the message. Either way, the confirmation process extracts the |
2682 | -token and uses that to confirm the pending registration. |
2683 | - |
2684 | - >>> registrar.confirm(token) |
2685 | - True |
2686 | - |
2687 | -Now, there is an `IAddress` in the database matching the address, as well as |
2688 | -an `IUser` linked to this address. The `IAddress` is verified. |
2689 | - |
2690 | - >>> found_address = user_manager.get_address('aperson@example.com') |
2691 | - >>> found_address |
2692 | - <Address: Anne Person <aperson@example.com> [verified] at ...> |
2693 | - >>> found_user = user_manager.get_user('aperson@example.com') |
2694 | - >>> found_user |
2695 | - <User "Anne Person" (...) at ...> |
2696 | - >>> found_user.controls(found_address.email) |
2697 | - True |
2698 | - >>> from datetime import datetime |
2699 | - >>> isinstance(found_address.verified_on, datetime) |
2700 | - True |
2701 | - |
2702 | - |
2703 | -Non-standard registrations |
2704 | -========================== |
2705 | - |
2706 | -If you try to confirm a registration token twice, of course only the first one |
2707 | -will work. The second one is ignored. |
2708 | - |
2709 | - >>> token = registrar.register(mlist, 'bperson@example.com') |
2710 | - >>> check_token(token) |
2711 | - ok |
2712 | - >>> items = get_queue_messages('virgin') |
2713 | - >>> len(items) |
2714 | - 1 |
2715 | - >>> sent_token = extract_token(items[0].msg) |
2716 | - >>> token == sent_token |
2717 | - True |
2718 | - >>> registrar.confirm(token) |
2719 | - True |
2720 | - >>> registrar.confirm(token) |
2721 | - False |
2722 | - |
2723 | -If an address is in the system, but that address is not linked to a user yet |
2724 | -and the address is not yet validated, then no user is created until the |
2725 | -confirmation step is completed. |
2726 | - |
2727 | - >>> user_manager.create_address('cperson@example.com') |
2728 | - <Address: cperson@example.com [not verified] at ...> |
2729 | - >>> token = registrar.register( |
2730 | - ... mlist, 'cperson@example.com', 'Claire Person') |
2731 | - >>> print(user_manager.get_user('cperson@example.com')) |
2732 | - None |
2733 | - >>> items = get_queue_messages('virgin') |
2734 | - >>> len(items) |
2735 | - 1 |
2736 | - >>> sent_token = extract_token(items[0].msg) |
2737 | - >>> registrar.confirm(sent_token) |
2738 | - True |
2739 | - >>> user_manager.get_user('cperson@example.com') |
2740 | - <User "Claire Person" (...) at ...> |
2741 | - >>> user_manager.get_address('cperson@example.com') |
2742 | - <Address: cperson@example.com [verified] at ...> |
2743 | - |
2744 | -Even if the address being registered has already been verified, the |
2745 | -registration sends a confirmation. |
2746 | - |
2747 | - >>> token = registrar.register(mlist, 'cperson@example.com') |
2748 | - >>> token is not None |
2749 | - True |
2750 | - |
2751 | - |
2752 | -Discarding |
2753 | -========== |
2754 | - |
2755 | -A confirmation token can also be discarded, say if the user changes his or her |
2756 | -mind about registering. When discarded, no `IAddress` or `IUser` is created. |
2757 | -:: |
2758 | - |
2759 | - >>> token = registrar.register(mlist, 'eperson@example.com', 'Elly Person') |
2760 | - >>> check_token(token) |
2761 | - ok |
2762 | - >>> registrar.discard(token) |
2763 | - >>> print(pendingdb.confirm(token)) |
2764 | - None |
2765 | - >>> print(user_manager.get_address('eperson@example.com')) |
2766 | - None |
2767 | - >>> print(user_manager.get_user('eperson@example.com')) |
2768 | - None |
2769 | - |
2770 | - # Clear the virgin queue of all the preceding confirmation messages. |
2771 | - >>> ignore = get_queue_messages('virgin') |
2772 | - |
2773 | - |
2774 | -Registering a new address for an existing user |
2775 | -============================================== |
2776 | - |
2777 | -When a new address for an existing user is registered, there isn't too much |
2778 | -different except that the new address will still need to be verified before it |
2779 | -can be used. |
2780 | -:: |
2781 | +When the registration steps involve confirmation or moderator approval, the |
2782 | +process will pause until these steps are completed. A unique token is created |
2783 | +which represents this work flow. |
2784 | + |
2785 | +Anne attempts to join the mailing list. |
2786 | + |
2787 | + >>> token = registrar.register(anne) |
2788 | + |
2789 | +Because her email address has not yet been verified, she has not yet become a |
2790 | +member of the mailing list. |
2791 | + |
2792 | + >>> print(mlist.members.get_member('anne@example.com')) |
2793 | + None |
2794 | + |
2795 | +Once she verifies her email address, she will become a member of the mailing |
2796 | +list. In this case, verifying implies that she also confirms her wish to join |
2797 | +the mailing list. |
2798 | + |
2799 | + >>> registrar.confirm(token) |
2800 | + >>> mlist.members.get_member('anne@example.com') |
2801 | + <Member: Anne Person <anne@example.com> on ant@example.com |
2802 | + as MemberRole.member> |
2803 | + |
2804 | + |
2805 | +Register a user |
2806 | +=============== |
2807 | + |
2808 | +Users can also register, but they must have a preferred address. The mailing |
2809 | +list will deliver messages to this preferred address. |
2810 | + |
2811 | + >>> bart = getUtility(IUserManager).make_user( |
2812 | + ... 'bart@example.com', 'Bart Person') |
2813 | + |
2814 | +Bart verifies his address and makes it his preferred address. |
2815 | |
2816 | >>> from mailman.utilities.datetime import now |
2817 | - >>> dperson = user_manager.create_user( |
2818 | - ... 'dperson@example.com', 'Dave Person') |
2819 | - >>> dperson |
2820 | - <User "Dave Person" (...) at ...> |
2821 | - >>> address = user_manager.get_address('dperson@example.com') |
2822 | - >>> address.verified_on = now() |
2823 | - |
2824 | - >>> from operator import attrgetter |
2825 | - >>> dump_list(repr(address) for address in dperson.addresses) |
2826 | - <Address: Dave Person <dperson@example.com> [verified] at ...> |
2827 | - >>> dperson.register('david.person@example.com', 'David Person') |
2828 | - <Address: David Person <david.person@example.com> [not verified] at ...> |
2829 | - >>> token = registrar.register(mlist, 'david.person@example.com') |
2830 | - |
2831 | - >>> items = get_queue_messages('virgin') |
2832 | - >>> len(items) |
2833 | - 1 |
2834 | - >>> sent_token = extract_token(items[0].msg) |
2835 | - >>> registrar.confirm(sent_token) |
2836 | - True |
2837 | - >>> user = user_manager.get_user('david.person@example.com') |
2838 | - >>> user is dperson |
2839 | - True |
2840 | - >>> user |
2841 | - <User "Dave Person" (...) at ...> |
2842 | - >>> dump_list(repr(address) for address in user.addresses) |
2843 | - <Address: Dave Person <dperson@example.com> [verified] at ...> |
2844 | - <Address: David Person <david.person@example.com> [verified] at ...> |
2845 | - |
2846 | - |
2847 | -Corner cases |
2848 | -============ |
2849 | - |
2850 | -If you try to confirm a token that doesn't exist in the pending database, the |
2851 | -confirm method will just return False. |
2852 | - |
2853 | - >>> registrar.confirm(bytes(b'no token')) |
2854 | - False |
2855 | - |
2856 | -Likewise, if you try to confirm, through the `IRegistrar` interface, a token |
2857 | -that doesn't match a registration event, you will get ``None``. However, the |
2858 | -pending event matched with that token will still be removed. |
2859 | -:: |
2860 | - |
2861 | - >>> from mailman.interfaces.pending import IPendable |
2862 | - >>> from zope.interface import implementer |
2863 | - |
2864 | - >>> @implementer(IPendable) |
2865 | - ... class SimplePendable(dict): |
2866 | - ... pass |
2867 | - |
2868 | - >>> pendable = SimplePendable(type='foo', bar='baz') |
2869 | - >>> token = pendingdb.add(pendable) |
2870 | - >>> registrar.confirm(token) |
2871 | - False |
2872 | - >>> print(pendingdb.confirm(token)) |
2873 | - None |
2874 | - |
2875 | - |
2876 | -Registration and subscription |
2877 | -============================= |
2878 | - |
2879 | -Fred registers with Mailman at the same time that he subscribes to a mailing |
2880 | -list. |
2881 | - |
2882 | - >>> token = registrar.register( |
2883 | - ... mlist, 'fred.person@example.com', 'Fred Person') |
2884 | - |
2885 | -Before confirmation, Fred is not a member of the mailing list. |
2886 | - |
2887 | - >>> print(mlist.members.get_member('fred.person@example.com')) |
2888 | - None |
2889 | - |
2890 | -But after confirmation, he is. |
2891 | - |
2892 | - >>> registrar.confirm(token) |
2893 | - True |
2894 | - >>> print(mlist.members.get_member('fred.person@example.com')) |
2895 | - <Member: Fred Person <fred.person@example.com> |
2896 | - on alpha@example.com as MemberRole.member> |
2897 | + >>> preferred = list(bart.addresses)[0] |
2898 | + >>> preferred.verified_on = now() |
2899 | + >>> bart.preferred_address = preferred |
2900 | + |
2901 | +The mailing list's subscription policy does not require Bart to confirm his |
2902 | +subscription, but the moderate does want to approve all subscriptions. |
2903 | + |
2904 | + >>> mlist.subscription_policy = SubscriptionPolicy.moderate |
2905 | + |
2906 | +Now when Bart registers as a user for the mailing list, a token will still be |
2907 | +generated, but this is only used by the moderator. At first, Bart is not |
2908 | +subscribed to the mailing list. |
2909 | + |
2910 | + >>> token = registrar.register(bart) |
2911 | + >>> print(mlist.members.get_member('bart@example.com')) |
2912 | + None |
2913 | + |
2914 | +When the moderator confirms Bart's subscription, he joins the mailing list. |
2915 | + |
2916 | + >>> registrar.confirm(token) |
2917 | + >>> mlist.members.get_member('bart@example.com') |
2918 | + <Member: Bart Person <bart@example.com> on ant@example.com |
2919 | + as MemberRole.member> |
2920 | |
2921 | === modified file 'src/mailman/model/mailinglist.py' |
2922 | --- src/mailman/model/mailinglist.py 2015-03-26 20:47:09 +0000 |
2923 | +++ src/mailman/model/mailinglist.py 2015-04-15 04:15:28 +0000 |
2924 | @@ -38,7 +38,7 @@ |
2925 | from mailman.interfaces.languages import ILanguageManager |
2926 | from mailman.interfaces.mailinglist import ( |
2927 | IAcceptableAlias, IAcceptableAliasSet, IListArchiver, IListArchiverSet, |
2928 | - IMailingList, Personalization, ReplyToMunging) |
2929 | + IMailingList, Personalization, ReplyToMunging, SubscriptionPolicy) |
2930 | from mailman.interfaces.member import ( |
2931 | AlreadySubscribedError, MemberRole, MissingPreferredAddressError, |
2932 | SubscriptionEvent) |
2933 | @@ -183,6 +183,7 @@ |
2934 | send_goodbye_message = Column(Boolean) |
2935 | send_welcome_message = Column(Boolean) |
2936 | subject_prefix = Column(Unicode) |
2937 | + subscription_policy = Column(Enum(SubscriptionPolicy)) |
2938 | topics = Column(PickleType) |
2939 | topics_bodylines_limit = Column(Integer) |
2940 | topics_enabled = Column(Boolean) |
2941 | |
2942 | === modified file 'src/mailman/model/member.py' |
2943 | --- src/mailman/model/member.py 2015-01-05 01:22:39 +0000 |
2944 | +++ src/mailman/model/member.py 2015-04-15 04:15:28 +0000 |
2945 | @@ -135,6 +135,10 @@ |
2946 | if self._address is None |
2947 | else getUtility(IUserManager).get_user(self._address.email)) |
2948 | |
2949 | + @property |
2950 | + def subscriber(self): |
2951 | + return (self._user if self._address is None else self._address) |
2952 | + |
2953 | def _lookup(self, preference, default=None): |
2954 | pref = getattr(self.preferences, preference) |
2955 | if pref is not None: |
2956 | |
2957 | === modified file 'src/mailman/model/pending.py' |
2958 | --- src/mailman/model/pending.py 2015-01-05 01:22:39 +0000 |
2959 | +++ src/mailman/model/pending.py 2015-04-15 04:15:28 +0000 |
2960 | @@ -128,7 +128,7 @@ |
2961 | return token |
2962 | |
2963 | @dbconnection |
2964 | - def confirm(self, store, token, expunge=True): |
2965 | + def confirm(self, store, token, *, expunge=True): |
2966 | # Token can come in as a unicode, but it's stored in the database as |
2967 | # bytes. They must be ascii. |
2968 | pendings = store.query(Pended).filter_by(token=str(token)) |
2969 | @@ -165,3 +165,7 @@ |
2970 | for keyvalue in q: |
2971 | store.delete(keyvalue) |
2972 | store.delete(pending) |
2973 | + |
2974 | + @dbconnection |
2975 | + def count(self, store): |
2976 | + return store.query(Pended).count() |
2977 | |
2978 | === modified file 'src/mailman/model/roster.py' |
2979 | --- src/mailman/model/roster.py 2015-01-05 01:22:39 +0000 |
2980 | +++ src/mailman/model/roster.py 2015-04-15 04:15:28 +0000 |
2981 | @@ -97,21 +97,48 @@ |
2982 | yield member.address |
2983 | |
2984 | @dbconnection |
2985 | - def get_member(self, store, address): |
2986 | - """See `IRoster`.""" |
2987 | - results = store.query(Member).filter( |
2988 | + def _get_all_memberships(self, store, email): |
2989 | + # Avoid circular imports. |
2990 | + from mailman.model.user import User |
2991 | + # Here's a query that finds all members subscribed with an explicit |
2992 | + # email address. |
2993 | + members_a = store.query(Member).filter( |
2994 | Member.list_id == self._mlist.list_id, |
2995 | Member.role == self.role, |
2996 | - Address.email == address, |
2997 | + Address.email == email, |
2998 | Member.address_id == Address.id) |
2999 | - if results.count() == 0: |
3000 | + # Here's a query that finds all members subscribed with their |
3001 | + # preferred address. |
3002 | + members_u = store.query(Member).filter( |
3003 | + Member.list_id == self._mlist.list_id, |
3004 | + Member.role == self.role, |
3005 | + Address.email==email, |
3006 | + Member.user_id == User.id) |
3007 | + return members_a.union(members_u).all() |
3008 | + |
3009 | + def get_member(self, email): |
3010 | + """See ``IRoster``.""" |
3011 | + memberships = self._get_all_memberships(email) |
3012 | + count = len(memberships) |
3013 | + if count == 0: |
3014 | return None |
3015 | - elif results.count() == 1: |
3016 | - return results[0] |
3017 | - else: |
3018 | - raise AssertionError( |
3019 | - 'Too many matching member results: {0}'.format( |
3020 | - results.count())) |
3021 | + elif count == 1: |
3022 | + return memberships[0] |
3023 | + assert count == 2, 'Unexpected membership count: {}'.format(count) |
3024 | + # This is the case where the email address is subscribed both |
3025 | + # explicitly and indirectly through the preferred address. By |
3026 | + # definition, we return the explicit address membership only. |
3027 | + return (memberships[0] |
3028 | + if memberships[0]._address is not None |
3029 | + else memberships[1]) |
3030 | + |
3031 | + def get_memberships(self, email): |
3032 | + """See ``IRoster``.""" |
3033 | + memberships = self._get_all_memberships(email) |
3034 | + count = len(memberships) |
3035 | + assert 0 <= count <= 2, 'Unexpected membership count: {}'.format( |
3036 | + count) |
3037 | + return memberships |
3038 | |
3039 | |
3040 | |
3041 | |
3042 | @@ -160,13 +187,13 @@ |
3043 | Member.role == MemberRole.moderator)) |
3044 | |
3045 | @dbconnection |
3046 | - def get_member(self, store, address): |
3047 | + def get_member(self, store, email): |
3048 | """See `IRoster`.""" |
3049 | results = store.query(Member).filter( |
3050 | Member.list_id == self._mlist.list_id, |
3051 | or_(Member.role == MemberRole.moderator, |
3052 | Member.role == MemberRole.owner), |
3053 | - Address.email == address, |
3054 | + Address.email == email, |
3055 | Member.address_id == Address.id) |
3056 | if results.count() == 0: |
3057 | return None |
3058 | @@ -181,6 +208,8 @@ |
3059 | class DeliveryMemberRoster(AbstractRoster): |
3060 | """Return all the members having a particular kind of delivery.""" |
3061 | |
3062 | + role = MemberRole.member |
3063 | + |
3064 | @property |
3065 | def member_count(self): |
3066 | """See `IRoster`.""" |
3067 | @@ -285,7 +314,7 @@ |
3068 | yield address |
3069 | |
3070 | @dbconnection |
3071 | - def get_member(self, store, address): |
3072 | + def get_member(self, store, email): |
3073 | """See `IRoster`.""" |
3074 | results = store.query(Member).filter( |
3075 | Member.address_id == Address.id, |
3076 | @@ -298,3 +327,10 @@ |
3077 | raise AssertionError( |
3078 | 'Too many matching member results: {0}'.format( |
3079 | results.count())) |
3080 | + |
3081 | + @dbconnection |
3082 | + def get_memberships(self, store, address): |
3083 | + """See `IRoster`.""" |
3084 | + # 2015-04-14 BAW: See LP: #1444055 -- this currently exists just to |
3085 | + # pass a test. |
3086 | + raise NotImplementedError |
3087 | |
3088 | === removed file 'src/mailman/model/tests/test_registrar.py' |
3089 | --- src/mailman/model/tests/test_registrar.py 2015-01-05 01:22:39 +0000 |
3090 | +++ src/mailman/model/tests/test_registrar.py 1970-01-01 00:00:00 +0000 |
3091 | @@ -1,64 +0,0 @@ |
3092 | -# Copyright (C) 2014-2015 by the Free Software Foundation, Inc. |
3093 | -# |
3094 | -# This file is part of GNU Mailman. |
3095 | -# |
3096 | -# GNU Mailman is free software: you can redistribute it and/or modify it under |
3097 | -# the terms of the GNU General Public License as published by the Free |
3098 | -# Software Foundation, either version 3 of the License, or (at your option) |
3099 | -# any later version. |
3100 | -# |
3101 | -# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT |
3102 | -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
3103 | -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
3104 | -# more details. |
3105 | -# |
3106 | -# You should have received a copy of the GNU General Public License along with |
3107 | -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. |
3108 | - |
3109 | -"""Test `IRegistrar`.""" |
3110 | - |
3111 | -__all__ = [ |
3112 | - 'TestRegistrar', |
3113 | - ] |
3114 | - |
3115 | - |
3116 | -import unittest |
3117 | - |
3118 | -from functools import partial |
3119 | -from mailman.app.lifecycle import create_list |
3120 | -from mailman.interfaces.address import InvalidEmailAddressError |
3121 | -from mailman.interfaces.registrar import IRegistrar |
3122 | -from mailman.testing.layers import ConfigLayer |
3123 | -from zope.component import getUtility |
3124 | - |
3125 | - |
3126 | - |
3127 | |
3128 | -class TestRegistrar(unittest.TestCase): |
3129 | - layer = ConfigLayer |
3130 | - |
3131 | - def setUp(self): |
3132 | - mlist = create_list('test@example.com') |
3133 | - self._register = partial(getUtility(IRegistrar).register, mlist) |
3134 | - |
3135 | - def test_invalid_empty_string(self): |
3136 | - self.assertRaises(InvalidEmailAddressError, self._register, '') |
3137 | - |
3138 | - def test_invalid_space_in_name(self): |
3139 | - self.assertRaises(InvalidEmailAddressError, self._register, |
3140 | - 'some name@example.com') |
3141 | - |
3142 | - def test_invalid_funky_characters(self): |
3143 | - self.assertRaises(InvalidEmailAddressError, self._register, |
3144 | - '<script>@example.com') |
3145 | - |
3146 | - def test_invalid_nonascii(self): |
3147 | - self.assertRaises(InvalidEmailAddressError, self._register, |
3148 | - '\xa0@example.com') |
3149 | - |
3150 | - def test_invalid_no_at_sign(self): |
3151 | - self.assertRaises(InvalidEmailAddressError, self._register, |
3152 | - 'noatsign') |
3153 | - |
3154 | - def test_invalid_no_domain(self): |
3155 | - self.assertRaises(InvalidEmailAddressError, self._register, |
3156 | - 'nodom@ain') |
3157 | |
3158 | === modified file 'src/mailman/model/tests/test_roster.py' |
3159 | --- src/mailman/model/tests/test_roster.py 2015-01-05 01:22:39 +0000 |
3160 | +++ src/mailman/model/tests/test_roster.py 2015-04-15 04:15:28 +0000 |
3161 | @@ -26,7 +26,9 @@ |
3162 | import unittest |
3163 | |
3164 | from mailman.app.lifecycle import create_list |
3165 | +from mailman.interfaces.address import IAddress |
3166 | from mailman.interfaces.member import DeliveryMode, MemberRole |
3167 | +from mailman.interfaces.user import IUser |
3168 | from mailman.interfaces.usermanager import IUserManager |
3169 | from mailman.testing.layers import ConfigLayer |
3170 | from mailman.utilities.datetime import now |
3171 | @@ -136,7 +138,8 @@ |
3172 | self._ant = create_list('ant@example.com') |
3173 | self._bee = create_list('bee@example.com') |
3174 | user_manager = getUtility(IUserManager) |
3175 | - self._anne = user_manager.create_user('anne@example.com') |
3176 | + self._anne = user_manager.make_user( |
3177 | + 'anne@example.com', 'Anne Person') |
3178 | preferred = list(self._anne.addresses)[0] |
3179 | preferred.verified_on = now() |
3180 | self._anne.preferred_address = preferred |
3181 | @@ -144,9 +147,56 @@ |
3182 | def test_no_memberships(self): |
3183 | # An unsubscribed user has no memberships. |
3184 | self.assertEqual(self._anne.memberships.member_count, 0) |
3185 | + self.assertIsNone(self._ant.members.get_member('anne@example.com')) |
3186 | + self.assertEqual( |
3187 | + self._ant.members.get_memberships('anne@example.com'), |
3188 | + []) |
3189 | |
3190 | def test_subscriptions(self): |
3191 | # Anne subscribes to a couple of mailing lists. |
3192 | self._ant.subscribe(self._anne) |
3193 | self._bee.subscribe(self._anne) |
3194 | self.assertEqual(self._anne.memberships.member_count, 2) |
3195 | + |
3196 | + def test_subscribed_as_user(self): |
3197 | + # Anne subscribes to a mailing list as a user and the member roster |
3198 | + # contains her membership. |
3199 | + self._ant.subscribe(self._anne) |
3200 | + self.assertEqual( |
3201 | + self._ant.members.get_member('anne@example.com').user, |
3202 | + self._anne) |
3203 | + memberships = self._ant.members.get_memberships('anne@example.com') |
3204 | + self.assertEqual( |
3205 | + [member.address.email for member in memberships], |
3206 | + ['anne@example.com']) |
3207 | + |
3208 | + def test_subscribed_as_user_and_address(self): |
3209 | + # Anne subscribes to a mailing list twice, once as a user and once |
3210 | + # with an explicit address. She has two memberships. |
3211 | + self._ant.subscribe(self._anne) |
3212 | + self._ant.subscribe(self._anne.preferred_address) |
3213 | + self.assertEqual(self._anne.memberships.member_count, 2) |
3214 | + self.assertEqual(self._ant.members.member_count, 2) |
3215 | + self.assertEqual( |
3216 | + [member.address.email for member in self._ant.members.members], |
3217 | + ['anne@example.com', 'anne@example.com']) |
3218 | + # get_member() is defined to return the explicit address. |
3219 | + member = self._ant.members.get_member('anne@example.com') |
3220 | + subscriber = member.subscriber |
3221 | + self.assertTrue(IAddress.providedBy(subscriber)) |
3222 | + self.assertFalse(IUser.providedBy(subscriber)) |
3223 | + # get_memberships() returns them all. |
3224 | + memberships = self._ant.members.get_memberships('anne@example.com') |
3225 | + self.assertEqual(len(memberships), 2) |
3226 | + as_address = (memberships[0] |
3227 | + if IAddress.providedBy(memberships[0].subscriber) |
3228 | + else memberships[1]) |
3229 | + as_user = (memberships[1] |
3230 | + if IUser.providedBy(memberships[1].subscriber) |
3231 | + else memberships[0]) |
3232 | + self.assertEqual(as_address.subscriber, self._anne.preferred_address) |
3233 | + self.assertEqual(as_user.subscriber, self._anne) |
3234 | + # All the email addresses match. |
3235 | + self.assertEqual( |
3236 | + [record.address.email for record in memberships], |
3237 | + ['anne@example.com', 'anne@example.com']) |
3238 | |
3239 | === modified file 'src/mailman/model/tests/test_usermanager.py' |
3240 | --- src/mailman/model/tests/test_usermanager.py 2015-03-22 01:43:50 +0000 |
3241 | +++ src/mailman/model/tests/test_usermanager.py 2015-04-15 04:15:28 +0000 |
3242 | @@ -80,3 +80,8 @@ |
3243 | user = self._usermanager.create_user('anne@example.com', 'Anne Person') |
3244 | other_user = self._usermanager.make_user('anne@example.com') |
3245 | self.assertIs(user, other_user) |
3246 | + |
3247 | + def test_get_user_by_id(self): |
3248 | + original = self._usermanager.make_user('anne@example.com') |
3249 | + copy = self._usermanager.get_user_by_id(original.user_id) |
3250 | + self.assertEqual(original, copy) |
3251 | |
3252 | === added file 'src/mailman/model/tests/test_workflow.py' |
3253 | --- src/mailman/model/tests/test_workflow.py 1970-01-01 00:00:00 +0000 |
3254 | +++ src/mailman/model/tests/test_workflow.py 2015-04-15 04:15:28 +0000 |
3255 | @@ -0,0 +1,148 @@ |
3256 | +# Copyright (C) 2015 by the Free Software Foundation, Inc. |
3257 | +# |
3258 | +# This file is part of GNU Mailman. |
3259 | +# |
3260 | +# GNU Mailman is free software: you can redistribute it and/or modify it under |
3261 | +# the terms of the GNU General Public License as published by the Free |
3262 | +# Software Foundation, either version 3 of the License, or (at your option) |
3263 | +# any later version. |
3264 | +# |
3265 | +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT |
3266 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
3267 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
3268 | +# more details. |
3269 | +# |
3270 | +# You should have received a copy of the GNU General Public License along with |
3271 | +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. |
3272 | + |
3273 | +"""Test the workflow model.""" |
3274 | + |
3275 | +__all__ = [ |
3276 | + 'TestWorkflow', |
3277 | + ] |
3278 | + |
3279 | + |
3280 | +import unittest |
3281 | + |
3282 | +from mailman.interfaces.workflow import IWorkflowStateManager |
3283 | +from mailman.testing.layers import ConfigLayer |
3284 | +from zope.component import getUtility |
3285 | + |
3286 | + |
3287 | + |
3288 | |
3289 | +class TestWorkflow(unittest.TestCase): |
3290 | + layer = ConfigLayer |
3291 | + |
3292 | + def setUp(self): |
3293 | + self._manager = getUtility(IWorkflowStateManager) |
3294 | + |
3295 | + def test_save_restore_workflow(self): |
3296 | + # Save and restore a workflow. |
3297 | + name = 'ant' |
3298 | + token = 'bee' |
3299 | + step = 'cat' |
3300 | + data = 'dog' |
3301 | + self._manager.save(name, token, step, data) |
3302 | + state = self._manager.restore(name, token) |
3303 | + self.assertEqual(state.name, name) |
3304 | + self.assertEqual(state.token, token) |
3305 | + self.assertEqual(state.step, step) |
3306 | + self.assertEqual(state.data, data) |
3307 | + |
3308 | + def test_save_restore_workflow_without_step(self): |
3309 | + # Save and restore a workflow that contains no step. |
3310 | + name = 'ant' |
3311 | + token = 'bee' |
3312 | + data = 'dog' |
3313 | + self._manager.save(name, token, data=data) |
3314 | + state = self._manager.restore(name, token) |
3315 | + self.assertEqual(state.name, name) |
3316 | + self.assertEqual(state.token, token) |
3317 | + self.assertIsNone(state.step) |
3318 | + self.assertEqual(state.data, data) |
3319 | + |
3320 | + def test_save_restore_workflow_without_data(self): |
3321 | + # Save and restore a workflow that contains no data. |
3322 | + name = 'ant' |
3323 | + token = 'bee' |
3324 | + step = 'cat' |
3325 | + self._manager.save(name, token, step) |
3326 | + state = self._manager.restore(name, token) |
3327 | + self.assertEqual(state.name, name) |
3328 | + self.assertEqual(state.token, token) |
3329 | + self.assertEqual(state.step, step) |
3330 | + self.assertIsNone(state.data) |
3331 | + |
3332 | + def test_save_restore_workflow_without_step_or_data(self): |
3333 | + # Save and restore a workflow that contains no step or data. |
3334 | + name = 'ant' |
3335 | + token = 'bee' |
3336 | + self._manager.save(name, token) |
3337 | + state = self._manager.restore(name, token) |
3338 | + self.assertEqual(state.name, name) |
3339 | + self.assertEqual(state.token, token) |
3340 | + self.assertIsNone(state.step) |
3341 | + self.assertIsNone(state.data) |
3342 | + |
3343 | + def test_restore_workflow_with_no_matching_name(self): |
3344 | + # Try to restore a workflow that has no matching name in the database. |
3345 | + name = 'ant' |
3346 | + token = 'bee' |
3347 | + self._manager.save(name, token) |
3348 | + state = self._manager.restore('ewe', token) |
3349 | + self.assertIsNone(state) |
3350 | + |
3351 | + def test_restore_workflow_with_no_matching_token(self): |
3352 | + # Try to restore a workflow that has no matching token in the database. |
3353 | + name = 'ant' |
3354 | + token = 'bee' |
3355 | + self._manager.save(name, token) |
3356 | + state = self._manager.restore(name, 'fly') |
3357 | + self.assertIsNone(state) |
3358 | + |
3359 | + def test_restore_workflow_with_no_matching_token_or_name(self): |
3360 | + # Try to restore a workflow that has no matching token or name in the |
3361 | + # database. |
3362 | + name = 'ant' |
3363 | + token = 'bee' |
3364 | + self._manager.save(name, token) |
3365 | + state = self._manager.restore('ewe', 'fly') |
3366 | + self.assertIsNone(state) |
3367 | + |
3368 | + def test_restore_removes_record(self): |
3369 | + name = 'ant' |
3370 | + token = 'bee' |
3371 | + self.assertEqual(self._manager.count, 0) |
3372 | + self._manager.save(name, token) |
3373 | + self.assertEqual(self._manager.count, 1) |
3374 | + self._manager.restore(name, token) |
3375 | + self.assertEqual(self._manager.count, 0) |
3376 | + |
3377 | + def test_save_after_restore(self): |
3378 | + name = 'ant' |
3379 | + token = 'bee' |
3380 | + self.assertEqual(self._manager.count, 0) |
3381 | + self._manager.save(name, token) |
3382 | + self.assertEqual(self._manager.count, 1) |
3383 | + self._manager.restore(name, token) |
3384 | + self.assertEqual(self._manager.count, 0) |
3385 | + self._manager.save(name, token) |
3386 | + self.assertEqual(self._manager.count, 1) |
3387 | + |
3388 | + def test_discard(self): |
3389 | + # Discard some workflow state. This is use by IRegistrar.discard(). |
3390 | + self._manager.save('ant', 'token', 'one') |
3391 | + self._manager.save('bee', 'token', 'two') |
3392 | + self._manager.save('ant', 'nekot', 'three') |
3393 | + self._manager.save('bee', 'nekot', 'four') |
3394 | + self.assertEqual(self._manager.count, 4) |
3395 | + self._manager.discard('bee', 'token') |
3396 | + self.assertEqual(self._manager.count, 3) |
3397 | + state = self._manager.restore('ant', 'token') |
3398 | + self.assertEqual(state.step, 'one') |
3399 | + state = self._manager.restore('bee', 'token') |
3400 | + self.assertIsNone(state) |
3401 | + state = self._manager.restore('ant', 'nekot') |
3402 | + self.assertEqual(state.step, 'three') |
3403 | + state = self._manager.restore('bee', 'nekot') |
3404 | + self.assertEqual(state.step, 'four') |
3405 | |
3406 | === modified file 'src/mailman/model/usermanager.py' |
3407 | --- src/mailman/model/usermanager.py 2015-03-22 01:32:12 +0000 |
3408 | +++ src/mailman/model/usermanager.py 2015-04-15 04:15:28 +0000 |
3409 | @@ -64,7 +64,6 @@ |
3410 | user.display_name = ( |
3411 | display_name if display_name else address.display_name) |
3412 | user.link(address) |
3413 | - return user |
3414 | return user |
3415 | |
3416 | @dbconnection |
3417 | |
3418 | === added file 'src/mailman/model/workflow.py' |
3419 | --- src/mailman/model/workflow.py 1970-01-01 00:00:00 +0000 |
3420 | +++ src/mailman/model/workflow.py 2015-04-15 04:15:28 +0000 |
3421 | @@ -0,0 +1,76 @@ |
3422 | +# Copyright (C) 2015 by the Free Software Foundation, Inc. |
3423 | +# |
3424 | +# This file is part of GNU Mailman. |
3425 | +# |
3426 | +# GNU Mailman is free software: you can redistribute it and/or modify it under |
3427 | +# the terms of the GNU General Public License as published by the Free |
3428 | +# Software Foundation, either version 3 of the License, or (at your option) |
3429 | +# any later version. |
3430 | +# |
3431 | +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT |
3432 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
3433 | +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
3434 | +# more details. |
3435 | +# |
3436 | +# You should have received a copy of the GNU General Public License along with |
3437 | +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. |
3438 | + |
3439 | +"""Model for workflow states.""" |
3440 | + |
3441 | +__all__ = [ |
3442 | + 'WorkflowState', |
3443 | + 'WorkflowStateManager', |
3444 | + ] |
3445 | + |
3446 | + |
3447 | +from mailman.database.model import Model |
3448 | +from mailman.database.transaction import dbconnection |
3449 | +from mailman.interfaces.workflow import IWorkflowState, IWorkflowStateManager |
3450 | +from sqlalchemy import Column, Unicode |
3451 | +from zope.interface import implementer |
3452 | + |
3453 | + |
3454 | + |
3455 | |
3456 | +@implementer(IWorkflowState) |
3457 | +class WorkflowState(Model): |
3458 | + """Workflow states.""" |
3459 | + |
3460 | + __tablename__ = 'workflowstate' |
3461 | + |
3462 | + name = Column(Unicode, primary_key=True) |
3463 | + token = Column(Unicode, primary_key=True) |
3464 | + step = Column(Unicode) |
3465 | + data = Column(Unicode) |
3466 | + |
3467 | + |
3468 | + |
3469 | |
3470 | +@implementer(IWorkflowStateManager) |
3471 | +class WorkflowStateManager: |
3472 | + """See `IWorkflowStateManager`.""" |
3473 | + |
3474 | + @dbconnection |
3475 | + def save(self, store, name, token, step=None, data=None): |
3476 | + """See `IWorkflowStateManager`.""" |
3477 | + state = WorkflowState(name=name, token=token, step=step, data=data) |
3478 | + store.add(state) |
3479 | + |
3480 | + @dbconnection |
3481 | + def restore(self, store, name, token): |
3482 | + """See `IWorkflowStateManager`.""" |
3483 | + state = store.query(WorkflowState).get((name, token)) |
3484 | + if state is not None: |
3485 | + store.delete(state) |
3486 | + return state |
3487 | + |
3488 | + @dbconnection |
3489 | + def discard(self, store, name, token): |
3490 | + """See `IWorkflowStateManager`.""" |
3491 | + state = store.query(WorkflowState).get((name, token)) |
3492 | + if state is not None: |
3493 | + store.delete(state) |
3494 | + |
3495 | + @property |
3496 | + @dbconnection |
3497 | + def count(self, store): |
3498 | + """See `IWorkflowStateManager`.""" |
3499 | + return store.query(WorkflowState).count() |
3500 | |
3501 | === modified file 'src/mailman/rest/docs/listconf.rst' |
3502 | --- src/mailman/rest/docs/listconf.rst 2015-01-05 01:20:33 +0000 |
3503 | +++ src/mailman/rest/docs/listconf.rst 2015-04-15 04:15:28 +0000 |
3504 | @@ -61,6 +61,7 @@ |
3505 | scheme: http |
3506 | send_welcome_message: True |
3507 | subject_prefix: [Ant] |
3508 | + subscription_policy: confirm |
3509 | volume: 1 |
3510 | web_host: lists.example.com |
3511 | welcome_message_uri: mailman:///welcome.txt |
3512 | @@ -106,6 +107,7 @@ |
3513 | ... reply_to_address='bee@example.com', |
3514 | ... send_welcome_message=False, |
3515 | ... subject_prefix='[ant]', |
3516 | + ... subscription_policy='moderate', |
3517 | ... welcome_message_uri='mailman:///welcome.txt', |
3518 | ... default_member_action='hold', |
3519 | ... default_nonmember_action='discard', |
3520 | @@ -156,6 +158,7 @@ |
3521 | ... |
3522 | send_welcome_message: False |
3523 | subject_prefix: [ant] |
3524 | + subscription_policy: moderate |
3525 | ... |
3526 | welcome_message_uri: mailman:///welcome.txt |
3527 | |
3528 | |
3529 | === modified file 'src/mailman/rest/listconf.py' |
3530 | --- src/mailman/rest/listconf.py 2015-04-07 01:39:34 +0000 |
3531 | +++ src/mailman/rest/listconf.py 2015-04-15 04:15:28 +0000 |
3532 | @@ -29,7 +29,8 @@ |
3533 | from mailman.interfaces.action import Action |
3534 | from mailman.interfaces.archiver import ArchivePolicy |
3535 | from mailman.interfaces.autorespond import ResponseAction |
3536 | -from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging |
3537 | +from mailman.interfaces.mailinglist import ( |
3538 | + IAcceptableAliasSet, ReplyToMunging, SubscriptionPolicy) |
3539 | from mailman.rest.helpers import ( |
3540 | GetterSetter, bad_request, etag, no_content, okay) |
3541 | from mailman.rest.validator import ( |
3542 | @@ -135,6 +136,7 @@ |
3543 | scheme=GetterSetter(None), |
3544 | send_welcome_message=GetterSetter(as_boolean), |
3545 | subject_prefix=GetterSetter(str), |
3546 | + subscription_policy=GetterSetter(enum_validator(SubscriptionPolicy)), |
3547 | volume=GetterSetter(None), |
3548 | web_host=GetterSetter(None), |
3549 | welcome_message_uri=GetterSetter(str), |
3550 | |
3551 | === modified file 'src/mailman/rest/tests/test_listconf.py' |
3552 | --- src/mailman/rest/tests/test_listconf.py 2015-01-05 01:22:39 +0000 |
3553 | +++ src/mailman/rest/tests/test_listconf.py 2015-04-15 04:15:28 +0000 |
3554 | @@ -79,6 +79,7 @@ |
3555 | reply_to_address='bee@example.com', |
3556 | send_welcome_message=False, |
3557 | subject_prefix='[ant]', |
3558 | + subscription_policy='confirm_then_moderate', |
3559 | welcome_message_uri='mailman:///welcome.txt', |
3560 | default_member_action='hold', |
3561 | default_nonmember_action='discard', |
3562 | |
3563 | === modified file 'src/mailman/runners/docs/command.rst' |
3564 | --- src/mailman/runners/docs/command.rst 2015-01-03 05:06:17 +0000 |
3565 | +++ src/mailman/runners/docs/command.rst 2015-04-15 04:15:28 +0000 |
3566 | @@ -141,15 +141,14 @@ |
3567 | 2 |
3568 | |
3569 | >>> from mailman.interfaces.registrar import IRegistrar |
3570 | - >>> from zope.component import getUtility |
3571 | - >>> registrar = getUtility(IRegistrar) |
3572 | + >>> registrar = IRegistrar(mlist) |
3573 | >>> for item in messages: |
3574 | ... subject = item.msg['subject'] |
3575 | ... print('Subject:', subject) |
3576 | ... if 'confirm' in str(subject): |
3577 | ... token = str(subject).split()[1].strip() |
3578 | - ... status = registrar.confirm(token) |
3579 | - ... assert status, 'Confirmation failed' |
3580 | + ... new_token = registrar.confirm(token) |
3581 | + ... assert new_token is None, 'Confirmation failed' |
3582 | Subject: The results of your email commands |
3583 | Subject: confirm ... |
3584 | |
3585 | |
3586 | === modified file 'src/mailman/runners/tests/test_confirm.py' |
3587 | --- src/mailman/runners/tests/test_confirm.py 2015-01-05 01:22:39 +0000 |
3588 | +++ src/mailman/runners/tests/test_confirm.py 2015-04-15 04:15:28 +0000 |
3589 | @@ -46,14 +46,14 @@ |
3590 | layer = ConfigLayer |
3591 | |
3592 | def setUp(self): |
3593 | - registrar = getUtility(IRegistrar) |
3594 | self._commandq = config.switchboards['command'] |
3595 | self._runner = make_testable_runner(CommandRunner, 'command') |
3596 | with transaction(): |
3597 | # Register a subscription requiring confirmation. |
3598 | self._mlist = create_list('test@example.com') |
3599 | self._mlist.send_welcome_message = False |
3600 | - self._token = registrar.register(self._mlist, 'anne@example.org') |
3601 | + anne = getUtility(IUserManager).create_address('anne@example.org') |
3602 | + self._token = IRegistrar(self._mlist).register(anne) |
3603 | |
3604 | def test_confirm_with_re_prefix(self): |
3605 | subject = 'Re: confirm {0}'.format(self._token) |
3606 | |
3607 | === modified file 'src/mailman/runners/tests/test_join.py' |
3608 | --- src/mailman/runners/tests/test_join.py 2015-01-05 01:22:39 +0000 |
3609 | +++ src/mailman/runners/tests/test_join.py 2015-04-15 04:15:28 +0000 |
3610 | @@ -160,8 +160,8 @@ |
3611 | subject_words = str(messages[1].msg['subject']).split() |
3612 | self.assertEqual(subject_words[0], 'confirm') |
3613 | token = subject_words[1] |
3614 | - status = getUtility(IRegistrar).confirm(token) |
3615 | - self.assertTrue(status, 'Confirmation failed') |
3616 | + status = IRegistrar(self._mlist).confirm(token) |
3617 | + self.assertIsNone(status, 'Confirmation failed') |
3618 | # Now, make sure that Anne is a member of the list and is receiving |
3619 | # digest deliveries. |
3620 | members = getUtility(ISubscriptionService).find_members( |
3621 | @@ -197,6 +197,8 @@ |
3622 | self.assertEqual(anne.address.email, 'anne@example.org') |
3623 | self.assertEqual(anne.delivery_mode, DeliveryMode.regular) |
3624 | |
3625 | + # LP: #1444184 - digest=mime is not currently supported. |
3626 | + @unittest.expectedFailure |
3627 | def test_join_with_mime_digests(self): |
3628 | # Test the digest=mime argument to the join command. |
3629 | msg = mfs("""\ |
3630 | @@ -211,6 +213,8 @@ |
3631 | self.assertEqual(anne.address.email, 'anne@example.org') |
3632 | self.assertEqual(anne.delivery_mode, DeliveryMode.mime_digests) |
3633 | |
3634 | + # LP: #1444184 - digest=mime is not currently supported. |
3635 | + @unittest.expectedFailure |
3636 | def test_join_with_plain_digests(self): |
3637 | # Test the digest=mime argument to the join command. |
3638 | msg = mfs("""\ |
3639 | |
3640 | === modified file 'src/mailman/styles/base.py' |
3641 | --- src/mailman/styles/base.py 2015-01-05 01:22:39 +0000 |
3642 | +++ src/mailman/styles/base.py 2015-04-15 04:15:28 +0000 |
3643 | @@ -41,7 +41,8 @@ |
3644 | from mailman.interfaces.autorespond import ResponseAction |
3645 | from mailman.interfaces.bounce import UnrecognizedBounceDisposition |
3646 | from mailman.interfaces.digests import DigestFrequency |
3647 | -from mailman.interfaces.mailinglist import Personalization, ReplyToMunging |
3648 | +from mailman.interfaces.mailinglist import ( |
3649 | + Personalization, ReplyToMunging, SubscriptionPolicy) |
3650 | from mailman.interfaces.nntp import NewsgroupModeration |
3651 | |
3652 | |
3653 | @@ -75,6 +76,7 @@ |
3654 | mlist.personalize = Personalization.none |
3655 | mlist.default_member_action = Action.defer |
3656 | mlist.default_nonmember_action = Action.hold |
3657 | + mlist.subscription_policy = SubscriptionPolicy.confirm |
3658 | # Notify the administrator of pending requests and membership changes. |
3659 | mlist.admin_immed_notify = True |
3660 | mlist.admin_notify_mchanges = False |
3661 | |
3662 | === modified file 'src/mailman/templates/en/confirm.txt' |
3663 | --- src/mailman/templates/en/confirm.txt 2012-03-04 08:08:52 +0000 |
3664 | +++ src/mailman/templates/en/confirm.txt 2015-04-15 04:15:28 +0000 |
3665 | @@ -8,9 +8,7 @@ |
3666 | |
3667 | Before you can start using GNU Mailman at this site, you must first confirm |
3668 | that this is your email address. You can do this by replying to this message, |
3669 | -keeping the Subject header intact. Or you can visit this web page |
3670 | - |
3671 | - $confirm_url |
3672 | +keeping the Subject header intact. |
3673 | |
3674 | If you do not wish to register this email address simply disregard this |
3675 | message. If you think you are being maliciously subscribed to the list, or |
3676 | |
3677 | === modified file 'src/mailman/templates/en/subauth.txt' |
3678 | --- src/mailman/templates/en/subauth.txt 2008-09-29 06:44:19 +0000 |
3679 | +++ src/mailman/templates/en/subauth.txt 2015-04-15 04:15:28 +0000 |
3680 | @@ -3,9 +3,3 @@ |
3681 | |
3682 | For: $username |
3683 | List: $listname |
3684 | - |
3685 | -At your convenience, visit: |
3686 | - |
3687 | - $admindb_url |
3688 | - |
3689 | -to process the request. |
3690 | |
3691 | === modified file 'src/mailman/utilities/importer.py' |
3692 | --- src/mailman/utilities/importer.py 2015-03-26 02:32:56 +0000 |
3693 | +++ src/mailman/utilities/importer.py 2015-04-15 04:15:28 +0000 |
3694 | @@ -41,6 +41,7 @@ |
3695 | from mailman.interfaces.languages import ILanguageManager |
3696 | from mailman.interfaces.mailinglist import IAcceptableAliasSet |
3697 | from mailman.interfaces.mailinglist import Personalization, ReplyToMunging |
3698 | +from mailman.interfaces.mailinglist import SubscriptionPolicy |
3699 | from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole |
3700 | from mailman.interfaces.nntp import NewsgroupModeration |
3701 | from mailman.interfaces.usermanager import IUserManager |
3702 | @@ -178,6 +179,7 @@ |
3703 | personalize=Personalization, |
3704 | preferred_language=check_language_code, |
3705 | reply_goes_to_list=ReplyToMunging, |
3706 | + subscription_policy=SubscriptionPolicy, |
3707 | topics_enabled=bool, |
3708 | ) |
3709 | |
3710 | @@ -202,6 +204,7 @@ |
3711 | real_name='display_name', |
3712 | send_goodbye_msg='send_goodbye_message', |
3713 | send_welcome_msg='send_welcome_message', |
3714 | + subscribe_policy='subscription_policy', |
3715 | ) |
3716 | |
3717 | # These DateTime fields of the mailinglist table need a type conversion to |
3718 | |
3719 | === modified file 'src/mailman/utilities/tests/test_import.py' |
3720 | --- src/mailman/utilities/tests/test_import.py 2015-03-26 02:32:56 +0000 |
3721 | +++ src/mailman/utilities/tests/test_import.py 2015-04-15 04:15:28 +0000 |
3722 | @@ -44,7 +44,8 @@ |
3723 | from mailman.interfaces.bans import IBanManager |
3724 | from mailman.interfaces.bounce import UnrecognizedBounceDisposition |
3725 | from mailman.interfaces.languages import ILanguageManager |
3726 | -from mailman.interfaces.mailinglist import IAcceptableAliasSet |
3727 | +from mailman.interfaces.mailinglist import ( |
3728 | + IAcceptableAliasSet, SubscriptionPolicy) |
3729 | from mailman.interfaces.member import DeliveryMode, DeliveryStatus |
3730 | from mailman.interfaces.nntp import NewsgroupModeration |
3731 | from mailman.interfaces.templates import ITemplateLoader |
3732 | @@ -301,6 +302,30 @@ |
3733 | self._import() |
3734 | self.assertEqual(self._mlist.encode_ascii_prefixes, True) |
3735 | |
3736 | + def test_subscription_policy_open(self): |
3737 | + self._mlist.subscription_policy = SubscriptionPolicy.confirm |
3738 | + self._pckdict['subscribe_policy'] = 0 |
3739 | + self._import() |
3740 | + self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.open) |
3741 | + |
3742 | + def test_subscription_policy_confirm(self): |
3743 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
3744 | + self._pckdict['subscribe_policy'] = 1 |
3745 | + self._import() |
3746 | + self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.confirm) |
3747 | + |
3748 | + def test_subscription_policy_moderate(self): |
3749 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
3750 | + self._pckdict['subscribe_policy'] = 2 |
3751 | + self._import() |
3752 | + self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.moderate) |
3753 | + |
3754 | + def test_subscription_policy_confirm_then_moderate(self): |
3755 | + self._mlist.subscription_policy = SubscriptionPolicy.open |
3756 | + self._pckdict['subscribe_policy'] = 3 |
3757 | + self._import() |
3758 | + self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.confirm_then_moderate) |
3759 | + |
3760 | |
3761 | |
3762 | |
3763 | class TestArchiveImport(unittest.TestCase): |
Good catches! I'll commit fixes momentarily. Thanks!