mailman  2.1.39
About: Mailman 2 - The GNU Mailing List Management System.
  Fossies Dox: mailman-2.1.39.tgz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

options.py
Go to the documentation of this file.
1# Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16# USA.
17
18"""Produce and handle the member options."""
19
20import re
21import sys
22import os
23import cgi
24import signal
25import urllib
26from types import ListType
27
28from Mailman import mm_cfg
29from Mailman import Utils
30from Mailman import MailList
31from Mailman import Errors
32from Mailman import MemberAdaptor
33from Mailman import i18n
34from Mailman.htmlformat import *
35from Mailman.Logging.Syslog import syslog
36from Mailman.CSRFcheck import csrf_check
37
38OR = '|'
39SLASH = '/'
40SETLANGUAGE = -1
41DIGRE = re.compile(
42 '<!--Start-Digests-Delete-->.*<!--End-Digests-Delete-->',
43 re.DOTALL)
44
45# Set up i18n
46_ = i18n._
47i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
48def D_(s):
49 return s
50
51try:
52 True, False
53except NameError:
54 True = 1
55 False = 0
56
57
58def main():
59 global _
60 doc = Document()
61 doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
62
63 method = Utils.GetRequestMethod()
64 if method.lower() not in ('get', 'post'):
65 title = _('CGI script error')
66 doc.SetTitle(title)
67 doc.AddItem(Header(2, title))
68 doc.addError(_('Invalid request method: %(method)s'))
69 doc.AddItem('<hr>')
70 doc.AddItem(MailmanLogo())
71 print 'Status: 405 Method Not Allowed'
72 print doc.Format()
73 return
74
75 parts = Utils.GetPathPieces()
76 lenparts = parts and len(parts)
77 if not parts or lenparts < 1:
78 title = _('CGI script error')
79 doc.SetTitle(title)
80 doc.AddItem(Header(2, title))
81 doc.addError(_('Invalid options to CGI script.'))
82 doc.AddItem('<hr>')
83 doc.AddItem(MailmanLogo())
84 print doc.Format()
85 return
86
87 # get the list and user's name
88 listname = parts[0].lower()
89 # open list
90 try:
91 mlist = MailList.MailList(listname, lock=0)
92 except Errors.MMListError, e:
93 # Avoid cross-site scripting attacks
94 safelistname = Utils.websafe(listname)
95 title = _('CGI script error')
96 doc.SetTitle(title)
97 doc.AddItem(Header(2, title))
98 doc.addError(_('No such list <em>%(safelistname)s</em>'))
99 doc.AddItem('<hr>')
100 doc.AddItem(MailmanLogo())
101 # Send this with a 404 status.
102 print 'Status: 404 Not Found'
103 print doc.Format()
104 syslog('error', 'options: No such list "%s": %s\n', listname, e)
105 return
106
107 # The total contents of the user's response
108 cgidata = cgi.FieldStorage(keep_blank_values=1)
109
110 # CSRF check
111 safe_params = ['displang-button', 'language', 'email', 'password', 'login',
112 'login-unsub', 'login-remind', 'VARHELP', 'UserOptions']
113 try:
114 params = cgidata.keys()
115 except TypeError:
116 # Someone crafted a POST with a bad Content-Type:.
117 doc.AddItem(Header(2, _("Error")))
118 doc.AddItem(Bold(_('Invalid options to CGI script.')))
119 # Send this with a 400 status.
120 print 'Status: 400 Bad Request'
121 print doc.Format()
122 return
123
124 # Set the language for the page. If we're coming from the listinfo cgi,
125 # we might have a 'language' key in the cgi data. That was an explicit
126 # preference to view the page in, so we should honor that here. If that's
127 # not available, use the list's default language.
128 language = cgidata.getfirst('language')
129 if not Utils.IsLanguage(language):
130 language = mlist.preferred_language
131 i18n.set_language(language)
132 doc.set_language(language)
133
134 if lenparts < 2:
135 user = cgidata.getfirst('email', '').strip()
136 if not user:
137 # If we're coming from the listinfo page and we left the email
138 # address field blank, it's not an error. Likewise if we're
139 # coming from anywhere else. Only issue the error if we came
140 # via one of our buttons.
141 if (cgidata.getfirst('login') or cgidata.getfirst('login-unsub')
142 or cgidata.getfirst('login-remind')):
143 doc.addError(_('No address given'))
144 loginpage(mlist, doc, None, language)
145 print doc.Format()
146 return
147 else:
148 user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:])))
149 # If a user submits a form or URL with post data or query fragments
150 # with multiple occurrences of the same variable, we can get a list
151 # here. Be as careful as possible.
152 # This is no longer required because of getfirst() above, but leave it.
153 if isinstance(user, list) or isinstance(user, tuple):
154 if len(user) == 0:
155 user = ''
156 else:
157 user = user[-1].strip()
158
159 safeuser = Utils.websafe(user)
160 try:
161 Utils.ValidateEmail(user)
163 doc.addError(_('Illegal Email Address'))
164 loginpage(mlist, doc, None, language)
165 print doc.Format()
166 return
167 # Sanity check the user, but only give the "no such member" error when
168 # using public rosters, otherwise, we'll leak membership information.
169 if not mlist.isMember(user):
170 if mlist.private_roster == 0:
171 doc.addError(_('No such member: %(safeuser)s.'))
172 loginpage(mlist, doc, None, language)
173 print doc.Format()
174 return
175
176 # Avoid cross-site scripting attacks
177 if set(params) - set(safe_params):
178 csrf_checked = csrf_check(mlist, cgidata.getfirst('csrf_token'),
179 Utils.UnobscureEmail(urllib.unquote(user)))
180 else:
181 csrf_checked = True
182 # if password is present, void cookie to force password authentication.
183 if cgidata.getfirst('password'):
184 os.environ['HTTP_COOKIE'] = ''
185 csrf_checked = True
186
187 # Find the case preserved email address (the one the user subscribed with)
188 lcuser = user.lower()
189 try:
190 cpuser = mlist.getMemberCPAddress(lcuser)
192 # This happens if the user isn't a member but we've got private rosters
193 cpuser = None
194 if lcuser == cpuser:
195 cpuser = None
196
197 # And now we know the user making the request, so set things up to for the
198 # user's stored preferred language, overridden by any form settings for
199 # their new language preference.
200 userlang = cgidata.getfirst('language')
201 if not Utils.IsLanguage(userlang):
202 userlang = mlist.getMemberLanguage(user)
203 doc.set_language(userlang)
204 i18n.set_language(userlang)
205
206 # Are we processing an unsubscription request from the login screen?
207 msgc = _('If you are a list member, a confirmation email has been sent.')
208 msgb = _('You already have a subscription pending confirmation')
209 msga = _("""If you are a list member, your unsubscription request has been
210 forwarded to the list administrator for approval.""")
211 if cgidata.has_key('login-unsub'):
212 # Because they can't supply a password for unsubscribing, we'll need
213 # to do the confirmation dance.
214 if mlist.isMember(user):
215 # We must acquire the list lock in order to pend a request.
216 try:
217 mlist.Lock()
218 # If unsubs require admin approval, then this request has to
219 # be held. Otherwise, send a confirmation.
220 if mlist.unsubscribe_policy:
221 mlist.HoldUnsubscription(user)
222 doc.addError(msga, tag='')
223 else:
224 ip = os.environ.get('HTTP_FORWARDED_FOR',
225 os.environ.get('HTTP_X_FORWARDED_FOR',
226 os.environ.get('REMOTE_ADDR',
227 'unidentified origin')))
228 mlist.ConfirmUnsubscription(user, userlang, remote=ip)
229 doc.addError(msgc, tag='')
230 mlist.Save()
232 doc.addError(msgb)
233 finally:
234 mlist.Unlock()
235 else:
236 # Not a member
237 if mlist.private_roster == 0:
238 # Public rosters
239 doc.addError(_('No such member: %(safeuser)s.'))
240 else:
241 syslog('mischief',
242 'Unsub attempt of non-member w/ private rosters: %s',
243 user)
244 if mlist.unsubscribe_policy:
245 doc.addError(msga, tag='')
246 else:
247 doc.addError(msgc, tag='')
248 loginpage(mlist, doc, user, language)
249 print doc.Format()
250 return
251
252 # Are we processing a password reminder from the login screen?
253 msg = _("""If you are a list member,
254 your password has been emailed to you.""")
255 if cgidata.has_key('login-remind'):
256 if mlist.isMember(user):
257 mlist.MailUserPassword(user)
258 doc.addError(msg, tag='')
259 else:
260 # Not a member
261 if mlist.private_roster == 0:
262 # Public rosters
263 doc.addError(_('No such member: %(safeuser)s.'))
264 else:
265 syslog('mischief',
266 'Reminder attempt of non-member w/ private rosters: %s',
267 user)
268 doc.addError(msg, tag='')
269 loginpage(mlist, doc, user, language)
270 print doc.Format()
271 return
272
273 # Get the password from the form.
274 password = cgidata.getfirst('password', '').strip()
275 # Check authentication. We need to know if the credentials match the user
276 # or the site admin, because they are the only ones who are allowed to
277 # change things globally. Specifically, the list admin may not change
278 # values globally.
279 if mm_cfg.ALLOW_SITE_ADMIN_COOKIES:
280 user_or_siteadmin_context = (mm_cfg.AuthUser, mm_cfg.AuthSiteAdmin)
281 else:
282 # Site and list admins are treated equal so that list admin can pass
283 # site admin test. :-(
284 user_or_siteadmin_context = (mm_cfg.AuthUser,)
285 is_user_or_siteadmin = mlist.WebAuthenticate(
286 user_or_siteadmin_context, password, user)
287 # Authenticate, possibly using the password supplied in the login page
288 if not is_user_or_siteadmin and \
289 not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
290 mm_cfg.AuthSiteAdmin),
291 password, user):
292 # Not authenticated, so throw up the login page again. If they tried
293 # to authenticate via cgi (instead of cookie), then print an error
294 # message.
295 if cgidata.has_key('password'):
296 doc.addError(_('Authentication failed.'))
297 remote = os.environ.get('HTTP_FORWARDED_FOR',
298 os.environ.get('HTTP_X_FORWARDED_FOR',
299 os.environ.get('REMOTE_ADDR',
300 'unidentified origin')))
301 syslog('security',
302 'Authorization failed (options): user=%s: list=%s: remote=%s',
303 user, listname, remote)
304 # So as not to allow membership leakage, prompt for the email
305 # address and the password here.
306 if mlist.private_roster <> 0:
307 syslog('mischief',
308 'Login failure with private rosters: %s from %s',
309 user, remote)
310 user = None
311 # give an HTTP 401 for authentication failure
312 print 'Status: 401 Unauthorized'
313 loginpage(mlist, doc, user, language)
314 print doc.Format()
315 return
316
317 # From here on out, the user is okay to view and modify their membership
318 # options. The first set of checks does not require the list to be
319 # locked.
320
321 # However, if a form is submitted for a user who has been asynchronously
322 # unsubscribed, uncaught NotAMemberError exceptions can be thrown.
323
324 if not mlist.isMember(user):
325 loginpage(mlist, doc, user, language)
326 print doc.Format()
327 return
328
329 # Before going further, get the result of CSRF check and do nothing
330 # if it has failed.
331 if csrf_checked == False:
332 doc.addError(
333 _('The form lifetime has expired. (request forgery check)'))
334 options_page(mlist, doc, user, cpuser, userlang)
335 print doc.Format()
336 return
337
338 # See if this is VARHELP on topics.
339 varhelp = None
340 if cgidata.has_key('VARHELP'):
341 varhelp = cgidata['VARHELP'].value
342 elif os.environ.get('QUERY_STRING'):
343 # POST methods, even if their actions have a query string, don't get
344 # put into FieldStorage's keys :-(
345 qs = cgi.parse_qs(os.environ['QUERY_STRING']).get('VARHELP')
346 if qs and type(qs) == types.ListType:
347 varhelp = qs[0]
348 if varhelp:
349 # Sanitize the topic name.
350 while '%' in varhelp:
351 varhelp = urllib.unquote_plus(varhelp)
352 varhelp = re.sub('<.*', '', varhelp)
353 topic_details(mlist, doc, user, cpuser, userlang, varhelp)
354 return
355
356 if cgidata.has_key('logout'):
357 print mlist.ZapCookie(mm_cfg.AuthUser, user)
358 loginpage(mlist, doc, user, language)
359 print doc.Format()
360 return
361
362 if cgidata.has_key('emailpw'):
363 mlist.MailUserPassword(user)
365 mlist, doc, user, cpuser, userlang,
366 _('A reminder of your password has been emailed to you.'))
367 print doc.Format()
368 return
369
370 if cgidata.has_key('othersubs'):
371 # Only the user or site administrator can view all subscriptions.
372 if not is_user_or_siteadmin:
373 doc.addError(_("""The list administrator may not view the other
374 subscriptions for this user."""), _('Note: '))
375 options_page(mlist, doc, user, cpuser, userlang)
376 print doc.Format()
377 return
378 hostname = mlist.host_name
379 title = _('List subscriptions for %(safeuser)s on %(hostname)s')
380 doc.SetTitle(title)
381 doc.AddItem(Header(2, title))
382 doc.AddItem(_('''Click on a link to visit your options page for the
383 requested mailing list.'''))
384
385 # Troll through all the mailing lists that match host_name and see if
386 # the user is a member. If so, add it to the list.
387 onlists = []
388 for gmlist in lists_of_member(mlist, user) + [mlist]:
389 extra = ''
390 url = gmlist.GetOptionsURL(user)
391 link = Link(url, gmlist.real_name)
392 if gmlist.getDeliveryStatus(user) <> MemberAdaptor.ENABLED:
393 extra += ', ' + _('nomail')
394 if user in gmlist.getDigestMemberKeys():
395 extra += ', ' + _('digest')
396 link = HTMLFormatObject(link, 0) + extra
397 onlists.append((gmlist.real_name, link))
398 onlists.sort()
399 items = OrderedList(*[link for name, link in onlists])
400 doc.AddItem(items)
401 print doc.Format()
402 return
403
404 if cgidata.has_key('change-of-address'):
405 # We could be changing the user's full name, email address, or both.
406 # Watch out for non-ASCII characters in the member's name.
407 membername = cgidata.getfirst('fullname')
408 # Canonicalize the member's name
409 membername = Utils.canonstr(membername, language)
410 newaddr = cgidata.getfirst('new-address')
411 confirmaddr = cgidata.getfirst('confirm-address')
412
413 oldname = mlist.getMemberName(user)
414 set_address = set_membername = 0
415
416 # See if the user wants to change their email address globally. The
417 # list admin is /not/ allowed to make global changes.
418 globally = cgidata.getfirst('changeaddr-globally')
419 if globally and not is_user_or_siteadmin:
420 doc.addError(_("""The list administrator may not change the names
421 or addresses for this user's other subscriptions. However, the
422 subscription for this mailing list has been changed."""),
423 _('Note: '))
424 globally = False
425 # We will change the member's name under the following conditions:
426 # - membername has a value
427 # - membername has no value, but they /used/ to have a membername
428 if membername and membername <> oldname:
429 # Setting it to a new value
430 set_membername = 1
431 if not membername and oldname:
432 # Unsetting it
433 set_membername = 1
434 # We will change the user's address if both newaddr and confirmaddr
435 # are non-blank, have the same value, and aren't the currently
436 # subscribed email address (when compared case-sensitively). If both
437 # are blank, but membername is set, we ignore it, otherwise we print
438 # an error.
439 msg = ''
440 if newaddr and confirmaddr:
441 if newaddr <> confirmaddr:
442 options_page(mlist, doc, user, cpuser, userlang,
443 _('Addresses did not match!'))
444 print doc.Format()
445 return
446 if newaddr == cpuser:
447 options_page(mlist, doc, user, cpuser, userlang,
448 _('You are already using that email address'))
449 print doc.Format()
450 return
451 # If they're requesting to subscribe an address which is already a
452 # member, and they're /not/ doing it globally, then refuse.
453 # Otherwise, we'll agree to do it globally (with a warning
454 # message) and let ApprovedChangeMemberAddress() handle already a
455 # member issues.
456 if mlist.isMember(newaddr):
457 safenewaddr = Utils.websafe(newaddr)
458 if globally:
459 listname = mlist.real_name
460 msg += _("""\
461The new address you requested %(newaddr)s is already a member of the
462%(listname)s mailing list, however you have also requested a global change of
463address. Upon confirmation, any other mailing list containing the address
464%(safeuser)s will be changed. """)
465 # Don't return
466 else:
468 mlist, doc, user, cpuser, userlang,
469 _('The new address is already a member: %(newaddr)s'))
470 print doc.Format()
471 return
472 set_address = 1
473 elif (newaddr or confirmaddr) and not set_membername:
474 options_page(mlist, doc, user, cpuser, userlang,
475 _('Addresses may not be blank'))
476 print doc.Format()
477 return
478
479 # Standard sigterm handler.
480 def sigterm_handler(signum, frame, mlist=mlist):
481 mlist.Unlock()
482 sys.exit(0)
483
484 signal.signal(signal.SIGTERM, sigterm_handler)
485 if set_address:
486 if cpuser is None:
487 cpuser = user
488 # Register the pending change after the list is locked
489 msg += _('A confirmation message has been sent to %(newaddr)s. ')
490 mlist.Lock()
491 try:
492 try:
493 mlist.ChangeMemberAddress(cpuser, newaddr, globally)
494 mlist.Save()
495 finally:
496 mlist.Unlock()
498 msg = _('Bad email address provided')
500 msg = _('Illegal email address provided')
502 msg = _('%(newaddr)s is already a member of the list.')
504 owneraddr = mlist.GetOwnerEmail()
505 msg = _("""%(newaddr)s is banned from this list. If you
506 think this restriction is erroneous, please contact
507 the list owners at %(owneraddr)s.""")
508
509 if set_membername:
510 mlist.Lock()
511 try:
512 mlist.ChangeMemberName(user, membername, globally)
513 mlist.Save()
514 finally:
515 mlist.Unlock()
516 msg += _('Member name successfully changed. ')
517
518 options_page(mlist, doc, user, cpuser, userlang, msg)
519 print doc.Format()
520 return
521
522 if cgidata.has_key('changepw'):
523 # Is this list admin and is list admin allowed to change passwords.
524 if not (is_user_or_siteadmin
525 or mm_cfg.OWNERS_CAN_CHANGE_MEMBER_PASSWORDS):
526 doc.addError(_("""The list administrator may not change the
527 password for a user."""))
528 options_page(mlist, doc, user, cpuser, userlang)
529 print doc.Format()
530 return
531 newpw = cgidata.getfirst('newpw', '').strip()
532 confirmpw = cgidata.getfirst('confpw', '').strip()
533 if not newpw or not confirmpw:
534 options_page(mlist, doc, user, cpuser, userlang,
535 _('Passwords may not be blank'))
536 print doc.Format()
537 return
538 if newpw <> confirmpw:
539 options_page(mlist, doc, user, cpuser, userlang,
540 _('Passwords did not match!'))
541 print doc.Format()
542 return
543
544 # See if the user wants to change their passwords globally, however
545 # the list admin is /not/ allowed to change passwords globally.
546 pw_globally = cgidata.getfirst('pw-globally')
547 if pw_globally and not is_user_or_siteadmin:
548 doc.addError(_("""The list administrator may not change the
549 password for this user's other subscriptions. However, the
550 password for this mailing list has been changed."""),
551 _('Note: '))
552 pw_globally = False
553
554 mlists = [mlist]
555
556 if pw_globally:
557 mlists.extend(lists_of_member(mlist, user))
558
559 for gmlist in mlists:
560 change_password(gmlist, user, newpw, confirmpw)
561
562 # Regenerate the cookie so a re-authorization isn't necessary
563 print mlist.MakeCookie(mm_cfg.AuthUser, user)
564 options_page(mlist, doc, user, cpuser, userlang,
565 _('Password successfully changed.'))
566 print doc.Format()
567 return
568
569 if cgidata.has_key('unsub'):
570 # Was the confirming check box turned on?
571 if not cgidata.getfirst('unsubconfirm'):
573 mlist, doc, user, cpuser, userlang,
574 _('''You must confirm your unsubscription request by turning
575 on the checkbox below the <em>Unsubscribe</em> button. You
576 have not been unsubscribed!'''))
577 print doc.Format()
578 return
579
580 # Standard signal handler
581 def sigterm_handler(signum, frame, mlist=mlist):
582 mlist.Unlock()
583 sys.exit(0)
584
585 # Okay, zap them. Leave them sitting at the list's listinfo page. We
586 # must own the list lock, and we want to make sure the user (BAW: and
587 # list admin?) is informed of the removal.
588 signal.signal(signal.SIGTERM, sigterm_handler)
589 mlist.Lock()
590 needapproval = False
591 try:
592 _ = D_
593 try:
594 mlist.DeleteMember(
595 user, _('via the member options page'), userack=1)
597 needapproval = True
599 # MAS This except should really be in the outer try so we
600 # don't save the list redundantly, but except and finally in
601 # the same try requires Python >= 2.5.
602 # Setting a switch and making the Save() conditional doesn't
603 # seem worth it as the Save() won't change anything.
604 pass
605 mlist.Save()
606 finally:
607 _ = i18n._
608 mlist.Unlock()
609 # Now throw up some results page, with appropriate links. We can't
610 # drop them back into their options page, because that's gone now!
611 fqdn_listname = mlist.GetListEmail()
612 owneraddr = mlist.GetOwnerEmail()
613 url = mlist.GetScriptURL('listinfo', absolute=1)
614
615 title = _('Unsubscription results')
616 doc.SetTitle(title)
617 doc.AddItem(Header(2, title))
618 if needapproval:
619 doc.AddItem(_("""Your unsubscription request has been received and
620 forwarded on to the list moderators for approval. You will
621 receive notification once the list moderators have made their
622 decision."""))
623 else:
624 doc.AddItem(_("""You have been successfully unsubscribed from the
625 mailing list %(fqdn_listname)s. If you were receiving digest
626 deliveries you may get one more digest. If you have any questions
627 about your unsubscription, please contact the list owners at
628 %(owneraddr)s."""))
629 doc.AddItem(mlist.GetMailmanFooter())
630 print doc.Format()
631 return
632
633 if cgidata.has_key('options-submit'):
634 # Digest action flags
635 digestwarn = 0
636 cantdigest = 0
637 mustdigest = 0
638
639 newvals = []
640 # First figure out which options have changed. The item names come
641 # from FormatOptionButton() in HTMLFormatter.py
642 for item, flag in (('digest', mm_cfg.Digests),
643 ('mime', mm_cfg.DisableMime),
644 ('dontreceive', mm_cfg.DontReceiveOwnPosts),
645 ('ackposts', mm_cfg.AcknowledgePosts),
646 ('disablemail', mm_cfg.DisableDelivery),
647 ('conceal', mm_cfg.ConcealSubscription),
648 ('remind', mm_cfg.SuppressPasswordReminder),
649 ('rcvtopic', mm_cfg.ReceiveNonmatchingTopics),
650 ('nodupes', mm_cfg.DontReceiveDuplicates),
651 ):
652 try:
653 newval = int(cgidata.getfirst(item))
654 except (TypeError, ValueError):
655 newval = None
656
657 # Skip this option if there was a problem or it wasn't changed.
658 # Note that delivery status is handled separate from the options
659 # flags.
660 if newval is None:
661 continue
662 elif flag == mm_cfg.DisableDelivery:
663 status = mlist.getDeliveryStatus(user)
664 # Here, newval == 0 means enable, newval == 1 means disable
665 if not newval and status <> MemberAdaptor.ENABLED:
666 newval = MemberAdaptor.ENABLED
667 elif newval and status == MemberAdaptor.ENABLED:
668 newval = MemberAdaptor.BYUSER
669 else:
670 continue
671 elif newval == mlist.getMemberOption(user, flag):
672 continue
673 # Should we warn about one more digest?
674 if flag == mm_cfg.Digests and \
675 newval == 0 and mlist.getMemberOption(user, flag):
676 digestwarn = 1
677
678 newvals.append((flag, newval))
679
680 # The user language is handled a little differently
681 if userlang not in mlist.GetAvailableLanguages():
682 newvals.append((SETLANGUAGE, mlist.preferred_language))
683 else:
684 newvals.append((SETLANGUAGE, userlang))
685
686 # Process user selected topics, but don't make the changes to the
687 # MailList object; we must do that down below when the list is
688 # locked.
689 topicnames = cgidata.getvalue('usertopic')
690 if topicnames:
691 # Some topics were selected. topicnames can actually be a string
692 # or a list of strings depending on whether more than one topic
693 # was selected or not.
694 if not isinstance(topicnames, ListType):
695 # Assume it was a bare string, so listify it
696 topicnames = [topicnames]
697 # unquote the topic names
698 topicnames = [urllib.unquote_plus(n) for n in topicnames]
699
700 # The standard sigterm handler (see above)
701 def sigterm_handler(signum, frame, mlist=mlist):
702 mlist.Unlock()
703 sys.exit(0)
704
705 # Now, lock the list and perform the changes
706 mlist.Lock()
707 try:
708 signal.signal(signal.SIGTERM, sigterm_handler)
709 # `values' is a tuple of flags and the web values
710 for flag, newval in newvals:
711 # Handle language settings differently
712 if flag == SETLANGUAGE:
713 mlist.setMemberLanguage(user, newval)
714 # Handle delivery status separately
715 elif flag == mm_cfg.DisableDelivery:
716 mlist.setDeliveryStatus(user, newval)
717 else:
718 try:
719 mlist.setMemberOption(user, flag, newval)
721 cantdigest = 1
723 mustdigest = 1
724 # Set the topics information.
725 mlist.setMemberTopics(user, topicnames)
726 mlist.Save()
727 finally:
728 mlist.Unlock()
729
730 # A bag of attributes for the global options
731 class Global:
732 enable = None
733 remind = None
734 nodupes = None
735 mime = None
736 def __nonzero__(self):
737 return len(self.__dict__.keys()) > 0
738
739 globalopts = Global()
740
741 # The enable/disable option and the password remind option may have
742 # their global flags sets.
743 if cgidata.getfirst('deliver-globally'):
744 # Yes, this is inefficient, but the list is so small it shouldn't
745 # make much of a difference.
746 for flag, newval in newvals:
747 if flag == mm_cfg.DisableDelivery:
748 globalopts.enable = newval
749 break
750
751 if cgidata.getfirst('remind-globally'):
752 for flag, newval in newvals:
753 if flag == mm_cfg.SuppressPasswordReminder:
754 globalopts.remind = newval
755 break
756
757 if cgidata.getfirst('nodupes-globally'):
758 for flag, newval in newvals:
759 if flag == mm_cfg.DontReceiveDuplicates:
760 globalopts.nodupes = newval
761 break
762
763 if cgidata.getfirst('mime-globally'):
764 for flag, newval in newvals:
765 if flag == mm_cfg.DisableMime:
766 globalopts.mime = newval
767 break
768
769 # Change options globally, but only if this is the user or site admin,
770 # /not/ if this is the list admin.
771 if globalopts:
772 if not is_user_or_siteadmin:
773 doc.addError(_("""The list administrator may not change the
774 options for this user's other subscriptions. However the
775 options for this mailing list subscription has been
776 changed."""), _('Note: '))
777 else:
778 for gmlist in lists_of_member(mlist, user):
779 global_options(gmlist, user, globalopts)
780
781 # Now print the results
782 if cantdigest:
783 msg = _('''The list administrator has disabled digest delivery for
784 this list, so your delivery option has not been set. However your
785 other options have been set successfully.''')
786 elif mustdigest:
787 msg = _('''The list administrator has disabled non-digest delivery
788 for this list, so your delivery option has not been set. However
789 your other options have been set successfully.''')
790 else:
791 msg = _('You have successfully set your options.')
792
793 if digestwarn:
794 msg += _('You may get one last digest.')
795
796 options_page(mlist, doc, user, cpuser, userlang, msg)
797 print doc.Format()
798 return
799
800 if mlist.isMember(user):
801 options_page(mlist, doc, user, cpuser, userlang)
802 else:
803 loginpage(mlist, doc, user, userlang)
804 print doc.Format()
805
806
807
808def options_page(mlist, doc, user, cpuser, userlang, message=''):
809 # The bulk of the document will come from the options.html template, which
810 # includes it's own html armor (head tags, etc.). Suppress the head that
811 # Document() derived pages get automatically.
812 doc.suppress_head = 1
813
814 if mlist.obscure_addresses:
815 presentable_user = Utils.ObscureEmail(user, for_text=1)
816 if cpuser is not None:
817 cpuser = Utils.ObscureEmail(cpuser, for_text=1)
818 else:
819 presentable_user = user
820
821 fullname = Utils.uncanonstr(mlist.getMemberName(user), userlang)
822 if fullname:
823 presentable_user += ', %s' % Utils.websafe(fullname)
824
825 # Do replacements
826 replacements = mlist.GetStandardReplacements(userlang)
827 replacements['<mm-results>'] = Bold(FontSize('+1', message)).Format()
828 replacements['<mm-digest-radio-button>'] = mlist.FormatOptionButton(
829 mm_cfg.Digests, 1, user)
830 replacements['<mm-undigest-radio-button>'] = mlist.FormatOptionButton(
831 mm_cfg.Digests, 0, user)
832 replacements['<mm-plain-digests-button>'] = mlist.FormatOptionButton(
833 mm_cfg.DisableMime, 1, user)
834 replacements['<mm-mime-digests-button>'] = mlist.FormatOptionButton(
835 mm_cfg.DisableMime, 0, user)
836 replacements['<mm-global-mime-button>'] = (
837 CheckBox('mime-globally', 1, checked=0).Format())
838 replacements['<mm-delivery-enable-button>'] = mlist.FormatOptionButton(
839 mm_cfg.DisableDelivery, 0, user)
840 replacements['<mm-delivery-disable-button>'] = mlist.FormatOptionButton(
841 mm_cfg.DisableDelivery, 1, user)
842 replacements['<mm-disabled-notice>'] = mlist.FormatDisabledNotice(user)
843 replacements['<mm-dont-ack-posts-button>'] = mlist.FormatOptionButton(
844 mm_cfg.AcknowledgePosts, 0, user)
845 replacements['<mm-ack-posts-button>'] = mlist.FormatOptionButton(
846 mm_cfg.AcknowledgePosts, 1, user)
847 replacements['<mm-receive-own-mail-button>'] = mlist.FormatOptionButton(
848 mm_cfg.DontReceiveOwnPosts, 0, user)
849 replacements['<mm-dont-receive-own-mail-button>'] = (
850 mlist.FormatOptionButton(mm_cfg.DontReceiveOwnPosts, 1, user))
851 replacements['<mm-dont-get-password-reminder-button>'] = (
852 mlist.FormatOptionButton(mm_cfg.SuppressPasswordReminder, 1, user))
853 replacements['<mm-get-password-reminder-button>'] = (
854 mlist.FormatOptionButton(mm_cfg.SuppressPasswordReminder, 0, user))
855 replacements['<mm-public-subscription-button>'] = (
856 mlist.FormatOptionButton(mm_cfg.ConcealSubscription, 0, user))
857 replacements['<mm-hide-subscription-button>'] = mlist.FormatOptionButton(
858 mm_cfg.ConcealSubscription, 1, user)
859 replacements['<mm-dont-receive-duplicates-button>'] = (
860 mlist.FormatOptionButton(mm_cfg.DontReceiveDuplicates, 1, user))
861 replacements['<mm-receive-duplicates-button>'] = (
862 mlist.FormatOptionButton(mm_cfg.DontReceiveDuplicates, 0, user))
863 replacements['<mm-unsubscribe-button>'] = (
864 mlist.FormatButton('unsub', _('Unsubscribe')) + '<br>' +
865 CheckBox('unsubconfirm', 1, checked=0).Format() +
866 _('<em>Yes, I really want to unsubscribe</em>'))
867 replacements['<mm-new-pass-box>'] = mlist.FormatSecureBox('newpw')
868 replacements['<mm-confirm-pass-box>'] = mlist.FormatSecureBox('confpw')
869 replacements['<mm-change-pass-button>'] = (
870 mlist.FormatButton('changepw', _("Change My Password")))
871 replacements['<mm-other-subscriptions-submit>'] = (
872 mlist.FormatButton('othersubs',
873 _('List my other subscriptions')))
874 replacements['<mm-form-start>'] = (
875 # Always make the CSRF token for the user. CVE-2021-42096
876 mlist.FormatFormStart('options', user, mlist=mlist,
877 contexts=[mm_cfg.AuthUser], user=user))
878 replacements['<mm-user>'] = user
879 replacements['<mm-presentable-user>'] = presentable_user
880 replacements['<mm-email-my-pw>'] = mlist.FormatButton(
881 'emailpw', (_('Email My Password To Me')))
882 replacements['<mm-umbrella-notice>'] = (
883 mlist.FormatUmbrellaNotice(user, _("password")))
884 replacements['<mm-logout-button>'] = (
885 mlist.FormatButton('logout', _('Log out')))
886 replacements['<mm-options-submit-button>'] = mlist.FormatButton(
887 'options-submit', _('Submit My Changes'))
888 replacements['<mm-global-pw-changes-button>'] = (
889 CheckBox('pw-globally', 1, checked=0).Format())
890 replacements['<mm-global-deliver-button>'] = (
891 CheckBox('deliver-globally', 1, checked=0).Format())
892 replacements['<mm-global-remind-button>'] = (
893 CheckBox('remind-globally', 1, checked=0).Format())
894 replacements['<mm-global-nodupes-button>'] = (
895 CheckBox('nodupes-globally', 1, checked=0).Format())
896
897 days = int(mm_cfg.PENDING_REQUEST_LIFE / mm_cfg.days(1))
898 if days > 1:
899 units = _('days')
900 else:
901 units = _('day')
902 replacements['<mm-pending-days>'] = _('%(days)d %(units)s')
903
904 replacements['<mm-new-address-box>'] = mlist.FormatBox('new-address')
905 replacements['<mm-confirm-address-box>'] = mlist.FormatBox(
906 'confirm-address')
907 replacements['<mm-change-address-button>'] = mlist.FormatButton(
908 'change-of-address', _('Change My Address and Name'))
909 replacements['<mm-global-change-of-address>'] = CheckBox(
910 'changeaddr-globally', 1, checked=0).Format()
911 replacements['<mm-fullname-box>'] = mlist.FormatBox(
912 'fullname', value=fullname)
913
914 # Create the topics radios. BAW: what if the list admin deletes a topic,
915 # but the user still wants to get that topic message?
916 usertopics = mlist.getMemberTopics(user)
917 if mlist.topics:
918 table = Table(border="0")
919 for name, pattern, description, emptyflag in mlist.topics:
920 if emptyflag:
921 continue
922 quotedname = urllib.quote_plus(name)
923 details = Link(mlist.GetScriptURL('options') +
924 '/%s/?VARHELP=%s' % (user, quotedname),
925 ' (Details)')
926 if name in usertopics:
927 checked = 1
928 else:
929 checked = 0
930 table.AddRow([CheckBox('usertopic', quotedname, checked=checked),
931 name + details.Format()])
932 topicsfield = table.Format()
933 else:
934 topicsfield = _('<em>No topics defined</em>')
935 replacements['<mm-topics>'] = topicsfield
936 replacements['<mm-suppress-nonmatching-topics>'] = (
937 mlist.FormatOptionButton(mm_cfg.ReceiveNonmatchingTopics, 0, user))
938 replacements['<mm-receive-nonmatching-topics>'] = (
939 mlist.FormatOptionButton(mm_cfg.ReceiveNonmatchingTopics, 1, user))
940
941 if cpuser is not None:
942 replacements['<mm-case-preserved-user>'] = _('''
943You are subscribed to this list with the case-preserved address
944<em>%(cpuser)s</em>.''')
945 else:
946 replacements['<mm-case-preserved-user>'] = ''
947
948 page_text = mlist.ParseTags('options.html', replacements, userlang)
949 if not (mlist.digestable or mlist.getMemberOption(user, mm_cfg.Digests)):
950 page_text = DIGRE.sub('', page_text)
951 doc.AddItem(page_text)
952
953
954def loginpage(mlist, doc, user, lang):
955 realname = mlist.real_name
956 actionurl = mlist.GetScriptURL('options')
957 if user is None:
958 title = _('%(realname)s list: member options login page')
959 extra = _('email address and ')
960 else:
961 safeuser = Utils.websafe(user)
962 title = _('%(realname)s list: member options for user %(safeuser)s')
963 obuser = Utils.ObscureEmail(user)
964 extra = ''
965 # Set up the title
966 doc.SetTitle(title)
967 # We use a subtable here so we can put a language selection box in
968 table = Table(width='100%', border=0, cellspacing=4, cellpadding=5)
969 # If only one language is enabled for this mailing list, omit the choice
970 # buttons.
971 table.AddRow([Center(Header(2, title))])
972 table.AddCellInfo(table.GetCurrentRowIndex(), 0,
973 bgcolor=mm_cfg.WEB_HEADER_COLOR)
974 if len(mlist.GetAvailableLanguages()) > 1:
975 langform = Form(actionurl)
976 langform.AddItem(SubmitButton('displang-button',
977 _('View this page in')))
978 langform.AddItem(mlist.GetLangSelectBox(lang))
979 if user:
980 langform.AddItem(Hidden('email', user))
981 table.AddRow([Center(langform)])
982 doc.AddItem(table)
983 # Preamble
984 # Set up the login page
985 form = Form(actionurl)
986 form.AddItem(Hidden('language', lang))
987 table = Table(width='100%', border=0, cellspacing=4, cellpadding=5)
988 table.AddRow([_("""In order to change your membership option, you must
989 first log in by giving your %(extra)smembership password in the section
990 below. If you don't remember your membership password, you can have it
991 emailed to you by clicking on the button below. If you just want to
992 unsubscribe from this list, click on the <em>Unsubscribe</em> button and a
993 confirmation message will be sent to you.
994
995 <p><strong><em>Important:</em></strong> From this point on, you must have
996 cookies enabled in your browser, otherwise none of your changes will take
997 effect.
998 """)])
999 # Password and login button
1000 ptable = Table(width='50%', border=0, cellspacing=4, cellpadding=5)
1001 if user is None:
1002 ptable.AddRow([Label(_('Email address:')),
1003 TextBox('email', size=20)])
1004 else:
1005 ptable.AddRow([Hidden('email', user)])
1006 ptable.AddRow([Label(_('Password:')),
1007 PasswordBox('password', size=20)])
1008 ptable.AddRow([Center(SubmitButton('login', _('Log in')))])
1009 ptable.AddCellInfo(ptable.GetCurrentRowIndex(), 0, colspan=2)
1010 table.AddRow([Center(ptable)])
1011 # Unsubscribe section
1012 table.AddRow([Center(Header(2, _('Unsubscribe')))])
1013 table.AddCellInfo(table.GetCurrentRowIndex(), 0,
1014 bgcolor=mm_cfg.WEB_HEADER_COLOR)
1015
1016 table.AddRow([_("""By clicking on the <em>Unsubscribe</em> button, a
1017 confirmation message will be emailed to you. This message will have a
1018 link that you should click on to complete the removal process (you can
1019 also confirm by email; see the instructions in the confirmation
1020 message).""")])
1021
1022 table.AddRow([Center(SubmitButton('login-unsub', _('Unsubscribe')))])
1023 # Password reminder section
1024 table.AddRow([Center(Header(2, _('Password reminder')))])
1025 table.AddCellInfo(table.GetCurrentRowIndex(), 0,
1026 bgcolor=mm_cfg.WEB_HEADER_COLOR)
1027
1028 table.AddRow([_("""By clicking on the <em>Remind</em> button, your
1029 password will be emailed to you.""")])
1030
1031 table.AddRow([Center(SubmitButton('login-remind', _('Remind')))])
1032 # Finish up glomming together the login page
1033 form.AddItem(table)
1034 doc.AddItem(form)
1035 doc.AddItem(mlist.GetMailmanFooter())
1036
1037
1038
1039def lists_of_member(mlist, user):
1040 hostname = mlist.host_name
1041 onlists = []
1042 for listname in Utils.list_names():
1043 # The current list will always handle things in the mainline
1044 if listname == mlist.internal_name():
1045 continue
1046 glist = MailList.MailList(listname, lock=0)
1047 if glist.host_name <> hostname:
1048 continue
1049 if not glist.isMember(user):
1050 continue
1051 onlists.append(glist)
1052 return onlists
1053
1054
1055
1056def change_password(mlist, user, newpw, confirmpw):
1057 # This operation requires the list lock, so let's set up the signal
1058 # handling so the list lock will get released when the user hits the
1059 # browser stop button.
1060 def sigterm_handler(signum, frame, mlist=mlist):
1061 # Make sure the list gets unlocked...
1062 mlist.Unlock()
1063 # ...and ensure we exit, otherwise race conditions could cause us to
1064 # enter MailList.Save() while we're in the unlocked state, and that
1065 # could be bad!
1066 sys.exit(0)
1067
1068 # Must own the list lock!
1069 mlist.Lock()
1070 try:
1071 # Install the emergency shutdown signal handler
1072 signal.signal(signal.SIGTERM, sigterm_handler)
1073 # change the user's password. The password must already have been
1074 # compared to the confirmpw and otherwise been vetted for
1075 # acceptability.
1076 mlist.setMemberPassword(user, newpw)
1077 mlist.Save()
1078 finally:
1079 mlist.Unlock()
1080
1081
1082
1083def global_options(mlist, user, globalopts):
1084 # Is there anything to do?
1085 for attr in dir(globalopts):
1086 if attr.startswith('_'):
1087 continue
1088 if getattr(globalopts, attr) is not None:
1089 break
1090 else:
1091 return
1092
1093 def sigterm_handler(signum, frame, mlist=mlist):
1094 # Make sure the list gets unlocked...
1095 mlist.Unlock()
1096 # ...and ensure we exit, otherwise race conditions could cause us to
1097 # enter MailList.Save() while we're in the unlocked state, and that
1098 # could be bad!
1099 sys.exit(0)
1100
1101 # Must own the list lock!
1102 mlist.Lock()
1103 try:
1104 # Install the emergency shutdown signal handler
1105 signal.signal(signal.SIGTERM, sigterm_handler)
1106
1107 if globalopts.enable is not None:
1108 mlist.setDeliveryStatus(user, globalopts.enable)
1109
1110 if globalopts.remind is not None:
1111 mlist.setMemberOption(user, mm_cfg.SuppressPasswordReminder,
1112 globalopts.remind)
1113
1114 if globalopts.nodupes is not None:
1115 mlist.setMemberOption(user, mm_cfg.DontReceiveDuplicates,
1116 globalopts.nodupes)
1117
1118 if globalopts.mime is not None:
1119 mlist.setMemberOption(user, mm_cfg.DisableMime, globalopts.mime)
1120
1121 mlist.Save()
1122 finally:
1123 mlist.Unlock()
1124
1125
1126
1127def topic_details(mlist, doc, user, cpuser, userlang, varhelp):
1128 # Find out which topic the user wants to get details of
1129 reflist = varhelp.split('/')
1130 name = None
1131 topicname = _('<missing>')
1132 if len(reflist) == 1:
1133 topicname = urllib.unquote_plus(reflist[0])
1134 for name, pattern, description, emptyflag in mlist.topics:
1135 if name == topicname:
1136 break
1137 else:
1138 name = None
1139
1140 if not name:
1141 options_page(mlist, doc, user, cpuser, userlang,
1142 _('Requested topic is not valid: %(topicname)s'))
1143 print doc.Format()
1144 return
1145
1146 table = Table(border=3, width='100%')
1147 table.AddRow([Center(Bold(_('Topic filter details')))])
1148 table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
1149 bgcolor=mm_cfg.WEB_SUBHEADER_COLOR)
1150 table.AddRow([Bold(Label(_('Name:'))),
1151 Utils.websafe(name)])
1152 table.AddRow([Bold(Label(_('Pattern (as regexp):'))),
1153 '<pre>' + Utils.websafe(OR.join(pattern.splitlines()))
1154 + '</pre>'])
1155 table.AddRow([Bold(Label(_('Description:'))),
1156 Utils.websafe(description)])
1157 # Make colors look nice
1158 for row in range(1, 4):
1159 table.AddCellInfo(row, 0, bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
1160
1161 options_page(mlist, doc, user, cpuser, userlang, table.Format())
1162 print doc.Format()
def csrf_check(mlist, token, cgi_user=None)
Definition: CSRFcheck.py:58
def options_page(mlist, doc, user, cpuser, userlang, message='')
Definition: options.py:808
def change_password(mlist, user, newpw, confirmpw)
Definition: options.py:1056
def global_options(mlist, user, globalopts)
Definition: options.py:1083
def loginpage(mlist, doc, user, lang)
Definition: options.py:954
def topic_details(mlist, doc, user, cpuser, userlang, varhelp)
Definition: options.py:1127
def lists_of_member(mlist, user)
Definition: options.py:1039
def HTMLFormatObject(item, indent)
Definition: htmlformat.py:46