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"""Process and produce the list-administration options forms."""
19
20# For Python 2.1.x compatibility
21from __future__ import nested_scopes
22
23import sys
24import os
25import re
26import cgi
27import urllib
28import signal
29from types import *
30
31from email.Utils import unquote, parseaddr, formataddr
32
33from Mailman import mm_cfg
34from Mailman import Utils
35from Mailman import Message
36from Mailman import MailList
37from Mailman import Errors
38from Mailman import MemberAdaptor
39from Mailman import i18n
40from Mailman.UserDesc import UserDesc
41from Mailman.htmlformat import *
42from Mailman.Cgi import Auth
43from Mailman.Logging.Syslog import syslog
44from Mailman.Utils import sha_new
45from Mailman.CSRFcheck import csrf_check
46
47# Set up i18n
48_ = i18n._
49i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
50def D_(s):
51    return s
52
53NL = '\n'
54OPTCOLUMNS = 11
55
56try:
57    True, False
58except NameError:
59    True = 1
60    False = 0
61
62AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin)
63
64
65
66def main():
67    # Try to find out which list is being administered
68    parts = Utils.GetPathPieces()
69    if not parts:
70        # None, so just do the admin overview and be done with it
71        admin_overview()
72        return
73    # Get the list object
74    listname = parts[0].lower()
75    try:
76        mlist = MailList.MailList(listname, lock=0)
77    except Errors.MMListError, e:
78        # Avoid cross-site scripting attacks
79        safelistname = Utils.websafe(listname)
80        # Send this with a 404 status.
81        print 'Status: 404 Not Found'
82        admin_overview(_('No such list <em>%(safelistname)s</em>'))
83        syslog('error', 'admin: No such list "%s": %s\n',
84               listname, e)
85        return
86    # Now that we know what list has been requested, all subsequent admin
87    # pages are shown in that list's preferred language.
88    i18n.set_language(mlist.preferred_language)
89    # If the user is not authenticated, we're done.
90    cgidata = cgi.FieldStorage(keep_blank_values=1)
91    try:
92        cgidata.getfirst('csrf_token', '')
93    except TypeError:
94        # Someone crafted a POST with a bad Content-Type:.
95        doc = Document()
96        doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
97        doc.AddItem(Header(2, _("Error")))
98        doc.AddItem(Bold(_('Invalid options to CGI script.')))
99        # Send this with a 400 status.
100        print 'Status: 400 Bad Request'
101        print doc.Format()
102        return
103
104    # CSRF check
105    safe_params = ['VARHELP', 'adminpw', 'admlogin',
106                   'letter', 'chunk', 'findmember',
107                   'legend']
108    params = cgidata.keys()
109    if set(params) - set(safe_params):
110        csrf_checked = csrf_check(mlist, cgidata.getfirst('csrf_token'),
111                                  'admin')
112    else:
113        csrf_checked = True
114    # if password is present, void cookie to force password authentication.
115    if cgidata.getfirst('adminpw'):
116        os.environ['HTTP_COOKIE'] = ''
117        csrf_checked = True
118
119    if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
120                                  mm_cfg.AuthSiteAdmin),
121                                 cgidata.getfirst('adminpw', '')):
122        if cgidata.has_key('adminpw'):
123            # This is a re-authorization attempt
124            msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
125            remote = os.environ.get('HTTP_FORWARDED_FOR',
126                     os.environ.get('HTTP_X_FORWARDED_FOR',
127                     os.environ.get('REMOTE_ADDR',
128                                    'unidentified origin')))
129            syslog('security',
130                   'Authorization failed (admin): list=%s: remote=%s',
131                   listname, remote)
132        else:
133            msg = ''
134        Auth.loginpage(mlist, 'admin', msg=msg)
135        return
136
137    # Which subcategory was requested?  Default is `general'
138    if len(parts) == 1:
139        category = 'general'
140        subcat = None
141    elif len(parts) == 2:
142        category = parts[1]
143        subcat = None
144    else:
145        category = parts[1]
146        subcat = parts[2]
147
148    # Is this a log-out request?
149    if category == 'logout':
150        # site-wide admin should also be able to logout.
151        if mlist.AuthContextInfo(mm_cfg.AuthSiteAdmin)[0] == 'site':
152            print mlist.ZapCookie(mm_cfg.AuthSiteAdmin)
153        print mlist.ZapCookie(mm_cfg.AuthListAdmin)
154        Auth.loginpage(mlist, 'admin', frontpage=1)
155        return
156
157    # Sanity check
158    if category not in mlist.GetConfigCategories().keys():
159        category = 'general'
160
161    # Is the request for variable details?
162    varhelp = None
163    qsenviron = os.environ.get('QUERY_STRING')
164    parsedqs = None
165    if qsenviron:
166        parsedqs = cgi.parse_qs(qsenviron)
167    if cgidata.has_key('VARHELP'):
168        varhelp = cgidata.getfirst('VARHELP')
169    elif parsedqs:
170        # POST methods, even if their actions have a query string, don't get
171        # put into FieldStorage's keys :-(
172        qs = parsedqs.get('VARHELP')
173        if qs and isinstance(qs, ListType):
174            varhelp = qs[0]
175    if varhelp:
176        option_help(mlist, varhelp)
177        return
178
179    # The html page document
180    doc = Document()
181    doc.set_language(mlist.preferred_language)
182
183    # From this point on, the MailList object must be locked.  However, we
184    # must release the lock no matter how we exit.  try/finally isn't enough,
185    # because of this scenario: user hits the admin page which may take a long
186    # time to render; user gets bored and hits the browser's STOP button;
187    # browser shuts down socket; server tries to write to broken socket and
188    # gets a SIGPIPE.  Under Apache 1.3/mod_cgi, Apache catches this SIGPIPE
189    # (I presume it is buffering output from the cgi script), then turns
190    # around and SIGTERMs the cgi process.  Apache waits three seconds and
191    # then SIGKILLs the cgi process.  We /must/ catch the SIGTERM and do the
192    # most reasonable thing we can in as short a time period as possible.  If
193    # we get the SIGKILL we're screwed (because it's uncatchable and we'll
194    # have no opportunity to clean up after ourselves).
195    #
196    # This signal handler catches the SIGTERM, unlocks the list, and then
197    # exits the process.  The effect of this is that the changes made to the
198    # MailList object will be aborted, which seems like the only sensible
199    # semantics.
200    #
201    # BAW: This may not be portable to other web servers or cgi execution
202    # models.
203    def sigterm_handler(signum, frame, mlist=mlist):
204        # Make sure the list gets unlocked...
205        mlist.Unlock()
206        # ...and ensure we exit, otherwise race conditions could cause us to
207        # enter MailList.Save() while we're in the unlocked state, and that
208        # could be bad!
209        sys.exit(0)
210
211    mlist.Lock()
212    try:
213        # Install the emergency shutdown signal handler
214        signal.signal(signal.SIGTERM, sigterm_handler)
215
216        if cgidata.keys():
217            if csrf_checked:
218                # There are options to change
219                change_options(mlist, category, subcat, cgidata, doc)
220            else:
221                doc.addError(
222                  _('The form lifetime has expired. (request forgery check)'))
223            # Let the list sanity check the changed values
224            mlist.CheckValues()
225        # Additional sanity checks
226        if not mlist.digestable and not mlist.nondigestable:
227            doc.addError(
228                _('''You have turned off delivery of both digest and
229                non-digest messages.  This is an incompatible state of
230                affairs.  You must turn on either digest delivery or
231                non-digest delivery or your mailing list will basically be
232                unusable.'''), tag=_('Warning: '))
233
234        dm = mlist.getDigestMemberKeys()
235        if not mlist.digestable and dm:
236            doc.addError(
237                _('''You have digest members, but digests are turned
238                off. Those people will not receive mail.
239                Affected member(s) %(dm)r.'''),
240                tag=_('Warning: '))
241        rm = mlist.getRegularMemberKeys()
242        if not mlist.nondigestable and rm:
243            doc.addError(
244                _('''You have regular list members but non-digestified mail is
245                turned off.  They will receive non-digestified mail until you
246                fix this problem. Affected member(s) %(rm)r.'''),
247                tag=_('Warning: '))
248        # Glom up the results page and print it out
249        show_results(mlist, doc, category, subcat, cgidata)
250        print doc.Format()
251        mlist.Save()
252    finally:
253        # Now be sure to unlock the list.  It's okay if we get a signal here
254        # because essentially, the signal handler will do the same thing.  And
255        # unlocking is unconditional, so it's not an error if we unlock while
256        # we're already unlocked.
257        mlist.Unlock()
258
259
260
261def admin_overview(msg=''):
262    # Show the administrative overview page, with the list of all the lists on
263    # this host.  msg is an optional error message to display at the top of
264    # the page.
265    #
266    # This page should be displayed in the server's default language, which
267    # should have already been set.
268    hostname = Utils.get_domain()
269    legend = _('%(hostname)s mailing lists - Admin Links')
270    # The html `document'
271    doc = Document()
272    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
273    doc.SetTitle(legend)
274    # The table that will hold everything
275    table = Table(border=0, width="100%")
276    table.AddRow([Center(Header(2, legend))])
277    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
278                      bgcolor=mm_cfg.WEB_HEADER_COLOR)
279    # Skip any mailing list that isn't advertised.
280    advertised = []
281    listnames = Utils.list_names()
282    listnames.sort()
283
284    for name in listnames:
285        try:
286            mlist = MailList.MailList(name, lock=0)
287        except Errors.MMUnknownListError:
288            # The list could have been deleted by another process.
289            continue
290        if mlist.advertised:
291            if mm_cfg.VIRTUAL_HOST_OVERVIEW and (
292                   mlist.web_page_url.find('/%s/' % hostname) == -1 and
293                   mlist.web_page_url.find('/%s:' % hostname) == -1):
294                # List is for different identity of this host - skip it.
295                continue
296            else:
297                advertised.append((mlist.GetScriptURL('admin'),
298                                   mlist.real_name,
299                                   Utils.websafe(mlist.GetDescription())))
300    # Greeting depends on whether there was an error or not
301    if msg:
302        greeting = FontAttr(msg, color="ff5060", size="+1")
303    else:
304        greeting = FontAttr(_('Welcome!'), size='+2')
305
306    welcome = []
307    mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
308    if not advertised:
309        welcome.extend([
310            greeting,
311            _('''<p>There currently are no publicly-advertised %(mailmanlink)s
312            mailing lists on %(hostname)s.'''),
313            ])
314    else:
315        welcome.extend([
316            greeting,
317            _('''<p>Below is the collection of publicly-advertised
318            %(mailmanlink)s mailing lists on %(hostname)s.  Click on a list
319            name to visit the configuration pages for that list.'''),
320            ])
321
322    creatorurl = Utils.ScriptURL('create')
323    mailman_owner = Utils.get_site_email()
324    extra = msg and _('right ') or ''
325    welcome.extend([
326        _('''To visit the administrators configuration page for an
327        unadvertised list, open a URL similar to this one, but with a '/' and
328        the %(extra)slist name appended.  If you have the proper authority,
329        you can also <a href="%(creatorurl)s">create a new mailing list</a>.
330
331        <p>General list information can be found at '''),
332        Link(Utils.ScriptURL('listinfo'),
333             _('the mailing list overview page')),
334        '.',
335        _('<p>(Send questions and comments to '),
336        Link('mailto:%s' % mailman_owner, mailman_owner),
337        '.)<p>',
338        ])
339
340    table.AddRow([Container(*welcome)])
341    table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
342
343    if advertised:
344        table.AddRow(['&nbsp;', '&nbsp;'])
345        table.AddRow([Bold(FontAttr(_('List'), size='+2')),
346                      Bold(FontAttr(_('Description'), size='+2'))
347                      ])
348        highlight = 1
349        for url, real_name, description in advertised:
350            table.AddRow(
351                [Link(url, Bold(real_name)),
352                      description or Italic(_('[no description available]'))])
353            if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR:
354                table.AddRowInfo(table.GetCurrentRowIndex(),
355                                 bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR)
356            highlight = not highlight
357
358    doc.AddItem(table)
359    doc.AddItem('<hr>')
360    doc.AddItem(MailmanLogo())
361    print doc.Format()
362
363
364
365def option_help(mlist, varhelp):
366    # The html page document
367    doc = Document()
368    doc.set_language(mlist.preferred_language)
369    # Find out which category and variable help is being requested for.
370    item = None
371    reflist = varhelp.split('/')
372    if len(reflist) >= 2:
373        category = subcat = None
374        if len(reflist) == 2:
375            category, varname = reflist
376        elif len(reflist) == 3:
377            category, subcat, varname = reflist
378        options = mlist.GetConfigInfo(category, subcat)
379        if options:
380            for i in options:
381                if i and i[0] == varname:
382                    item = i
383                    break
384    # Print an error message if we couldn't find a valid one
385    if not item:
386        bad = _('No valid variable name found.')
387        doc.addError(bad)
388        doc.AddItem(mlist.GetMailmanFooter())
389        print doc.Format()
390        return
391    # Get the details about the variable
392    varname, kind, params, dependancies, description, elaboration = \
393             get_item_characteristics(item)
394    # Set up the document
395    realname = mlist.real_name
396    legend = _("""%(realname)s Mailing list Configuration Help
397    <br><em>%(varname)s</em> Option""")
398
399    header = Table(width='100%')
400    header.AddRow([Center(Header(3, legend))])
401    header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
402                       bgcolor=mm_cfg.WEB_HEADER_COLOR)
403    doc.SetTitle(_("Mailman %(varname)s List Option Help"))
404    doc.AddItem(header)
405    doc.AddItem("<b>%s</b> (%s): %s<p>" % (varname, category, description))
406    if elaboration:
407        doc.AddItem("%s<p>" % elaboration)
408
409    if subcat:
410        url = '%s/%s/%s' % (mlist.GetScriptURL('admin'), category, subcat)
411    else:
412        url = '%s/%s' % (mlist.GetScriptURL('admin'), category)
413    form = Form(url, mlist=mlist, contexts=AUTH_CONTEXTS)
414    valtab = Table(cellspacing=3, cellpadding=4, width='100%')
415    add_options_table_item(mlist, category, subcat, valtab, item, detailsp=0)
416    form.AddItem(valtab)
417    form.AddItem('<p>')
418    form.AddItem(Center(submit_button()))
419    doc.AddItem(Center(form))
420
421    doc.AddItem(_("""<em><strong>Warning:</strong> changing this option here
422    could cause other screens to be out-of-sync.  Be sure to reload any other
423    pages that are displaying this option for this mailing list.  You can also
424    """))
425
426    adminurl = mlist.GetScriptURL('admin')
427    if subcat:
428        url = '%s/%s/%s' % (adminurl, category, subcat)
429    else:
430        url = '%s/%s' % (adminurl, category)
431    categoryname = mlist.GetConfigCategories()[category][0]
432    doc.AddItem(Link(url, _('return to the %(categoryname)s options page.')))
433    doc.AddItem('</em>')
434    doc.AddItem(mlist.GetMailmanFooter())
435    print doc.Format()
436
437
438
439def show_results(mlist, doc, category, subcat, cgidata):
440    # Produce the results page
441    adminurl = mlist.GetScriptURL('admin')
442    categories = mlist.GetConfigCategories()
443    label = _(categories[category][0])
444
445    # Set up the document's headers
446    realname = mlist.real_name
447    doc.SetTitle(_('%(realname)s Administration (%(label)s)'))
448    doc.AddItem(Center(Header(2, _(
449        '%(realname)s mailing list administration<br>%(label)s Section'))))
450    doc.AddItem('<hr>')
451    # Now we need to craft the form that will be submitted, which will contain
452    # all the variable settings, etc.  This is a bit of a kludge because we
453    # know that the autoreply and members categories supports file uploads.
454    encoding = None
455    if category in ('autoreply', 'members'):
456        encoding = 'multipart/form-data'
457    if subcat:
458        form = Form('%s/%s/%s' % (adminurl, category, subcat),
459                    encoding=encoding, mlist=mlist, contexts=AUTH_CONTEXTS)
460    else:
461        form = Form('%s/%s' % (adminurl, category),
462                    encoding=encoding, mlist=mlist, contexts=AUTH_CONTEXTS)
463    # This holds the two columns of links
464    linktable = Table(valign='top', width='100%')
465    linktable.AddRow([Center(Bold(_("Configuration Categories"))),
466                      Center(Bold(_("Other Administrative Activities")))])
467    # The `other links' are stuff in the right column.
468    otherlinks = UnorderedList()
469    otherlinks.AddItem(Link(mlist.GetScriptURL('admindb'),
470                            _('Tend to pending moderator requests')))
471    otherlinks.AddItem(Link(mlist.GetScriptURL('listinfo'),
472                            _('Go to the general list information page')))
473    otherlinks.AddItem(Link(mlist.GetScriptURL('edithtml'),
474                            _('Edit the public HTML pages and text files')))
475    otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(),
476                            _('Go to list archives')).Format() +
477                       '<br>&nbsp;<br>')
478    # We do not allow through-the-web deletion of the site list!
479    if mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS and \
480           mlist.internal_name() <> mm_cfg.MAILMAN_SITE_LIST:
481        otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'),
482                                _('Delete this mailing list')).Format() +
483                           _(' (requires confirmation)<br>&nbsp;<br>'))
484    otherlinks.AddItem(Link('%s/logout' % adminurl,
485                            # BAW: What I really want is a blank line, but
486                            # adding an &nbsp; won't do it because of the
487                            # bullet added to the list item.
488                            '<FONT SIZE="+2"><b>%s</b></FONT>' %
489                            _('Logout')))
490    # These are links to other categories and live in the left column
491    categorylinks_1 = categorylinks = UnorderedList()
492    categorylinks_2 = ''
493    categorykeys = categories.keys()
494    half = len(categorykeys) / 2
495    counter = 0
496    subcat = None
497    for k in categorykeys:
498        label = _(categories[k][0])
499        url = '%s/%s' % (adminurl, k)
500        if k == category:
501            # Handle subcategories
502            subcats = mlist.GetConfigSubCategories(k)
503            if subcats:
504                subcat = Utils.GetPathPieces()[-1]
505                for k, v in subcats:
506                    if k == subcat:
507                        break
508                else:
509                    # The first subcategory in the list is the default
510                    subcat = subcats[0][0]
511                subcat_items = []
512                for sub, text in subcats:
513                    if sub == subcat:
514                        text = Bold('[%s]' % text).Format()
515                    subcat_items.append(Link(url + '/' + sub, text))
516                categorylinks.AddItem(
517                    Bold(label).Format() +
518                    UnorderedList(*subcat_items).Format())
519            else:
520                categorylinks.AddItem(Link(url, Bold('[%s]' % label)))
521        else:
522            categorylinks.AddItem(Link(url, label))
523        counter += 1
524        if counter >= half:
525            categorylinks_2 = categorylinks = UnorderedList()
526            counter = -len(categorykeys)
527    # Make the emergency stop switch a rude solo light
528    etable = Table()
529    # Add all the links to the links table...
530    etable.AddRow([categorylinks_1, categorylinks_2])
531    etable.AddRowInfo(etable.GetCurrentRowIndex(), valign='top')
532    if mlist.emergency:
533        label = _('Emergency moderation of all list traffic is enabled')
534        etable.AddRow([Center(
535            Link('?VARHELP=general/emergency', Bold(label)))])
536        color = mm_cfg.WEB_ERROR_COLOR
537        etable.AddCellInfo(etable.GetCurrentRowIndex(), 0,
538                           colspan=2, bgcolor=color)
539    linktable.AddRow([etable, otherlinks])
540    # ...and add the links table to the document.
541    form.AddItem(linktable)
542    form.AddItem('<hr>')
543    form.AddItem(
544        _('''Make your changes in the following section, then submit them
545        using the <em>Submit Your Changes</em> button below.''')
546        + '<p>')
547
548    # The members and passwords categories are special in that they aren't
549    # defined in terms of gui elements.  Create those pages here.
550    if category == 'members':
551        # Figure out which subcategory we should display
552        subcat = Utils.GetPathPieces()[-1]
553        if subcat not in ('list', 'add', 'remove', 'change', 'sync'):
554            subcat = 'list'
555        # Add member category specific tables
556        form.AddItem(membership_options(mlist, subcat, cgidata, doc, form))
557        form.AddItem(Center(submit_button('setmemberopts_btn')))
558        # In "list" subcategory, we can also search for members
559        if subcat == 'list':
560            form.AddItem('<hr>\n')
561            table = Table(width='100%')
562            table.AddRow([Center(Header(2, _('Additional Member Tasks')))])
563            table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
564                              bgcolor=mm_cfg.WEB_HEADER_COLOR)
565            # Add a blank separator row
566            table.AddRow(['&nbsp;', '&nbsp;'])
567            # Add a section to set the moderation bit for all members
568            table.AddRow([_("""<li>Set everyone's moderation bit, including
569            those members not currently visible""")])
570            table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
571            table.AddRow([RadioButtonArray('allmodbit_val',
572                                           (_('Off'), _('On')),
573                                           mlist.default_member_moderation),
574                          SubmitButton('allmodbit_btn', _('Set'))])
575            form.AddItem(table)
576    elif category == 'passwords':
577        form.AddItem(Center(password_inputs(mlist)))
578        form.AddItem(Center(submit_button()))
579    else:
580        form.AddItem(show_variables(mlist, category, subcat, cgidata, doc))
581        form.AddItem(Center(submit_button()))
582    # And add the form
583    doc.AddItem(form)
584    doc.AddItem(mlist.GetMailmanFooter())
585
586
587
588def show_variables(mlist, category, subcat, cgidata, doc):
589    options = mlist.GetConfigInfo(category, subcat)
590
591    # The table containing the results
592    table = Table(cellspacing=3, cellpadding=4, width='100%')
593
594    # Get and portray the text label for the category.
595    categories = mlist.GetConfigCategories()
596    label = _(categories[category][0])
597
598    table.AddRow([Center(Header(2, label))])
599    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
600                      bgcolor=mm_cfg.WEB_HEADER_COLOR)
601
602    # The very first item in the config info will be treated as a general
603    # description if it is a string
604    description = options[0]
605    if isinstance(description, StringType):
606        table.AddRow([description])
607        table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
608        options = options[1:]
609
610    if not options:
611        return table
612
613    # Add the global column headers
614    table.AddRow([Center(Bold(_('Description'))),
615                  Center(Bold(_('Value')))])
616    table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0,
617                      width='15%')
618    table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1,
619                      width='85%')
620
621    for item in options:
622        if type(item) == StringType:
623            # The very first banner option (string in an options list) is
624            # treated as a general description, while any others are
625            # treated as section headers - centered and italicized...
626            table.AddRow([Center(Italic(item))])
627            table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
628        else:
629            add_options_table_item(mlist, category, subcat, table, item)
630    table.AddRow(['<br>'])
631    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
632    return table
633
634
635
636def add_options_table_item(mlist, category, subcat, table, item, detailsp=1):
637    # Add a row to an options table with the item description and value.
638    varname, kind, params, extra, descr, elaboration = \
639             get_item_characteristics(item)
640    if elaboration is None:
641        elaboration = descr
642    descr = get_item_gui_description(mlist, category, subcat,
643                                     varname, descr, elaboration, detailsp)
644    val = get_item_gui_value(mlist, category, kind, varname, params, extra)
645    table.AddRow([descr, val])
646    table.AddCellInfo(table.GetCurrentRowIndex(), 0,
647                      bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
648    table.AddCellInfo(table.GetCurrentRowIndex(), 1,
649                      bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
650
651
652
653def get_item_characteristics(record):
654    # Break out the components of an item description from its description
655    # record:
656    #
657    # 0 -- option-var name
658    # 1 -- type
659    # 2 -- entry size
660    # 3 -- ?dependancies?
661    # 4 -- Brief description
662    # 5 -- Optional description elaboration
663    if len(record) == 5:
664        elaboration = None
665        varname, kind, params, dependancies, descr = record
666    elif len(record) == 6:
667        varname, kind, params, dependancies, descr, elaboration = record
668    else:
669        raise ValueError, _('Badly formed options entry:\n %(record)s')
670    return varname, kind, params, dependancies, descr, elaboration
671
672
673
674def get_item_gui_value(mlist, category, kind, varname, params, extra):
675    """Return a representation of an item's settings."""
676    # Give the category a chance to return the value for the variable
677    value = None
678    label, gui = mlist.GetConfigCategories()[category]
679    if hasattr(gui, 'getValue'):
680        value = gui.getValue(mlist, kind, varname, params)
681    # Filter out None, and volatile attributes
682    if value is None and not varname.startswith('_'):
683        value = getattr(mlist, varname)
684    # Now create the widget for this value
685    if kind == mm_cfg.Radio or kind == mm_cfg.Toggle:
686        # If we are returning the option for subscribe policy and this site
687        # doesn't allow open subscribes, then we have to alter the value of
688        # mlist.subscribe_policy as passed to RadioButtonArray in order to
689        # compensate for the fact that there is one fewer option.
690        # Correspondingly, we alter the value back in the change options
691        # function -scott
692        #
693        # TBD: this is an ugly ugly hack.
694        if varname.startswith('_'):
695            checked = 0
696        else:
697            checked = value
698        if varname == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
699            checked = checked - 1
700        # For Radio buttons, we're going to interpret the extra stuff as a
701        # horizontal/vertical flag.  For backwards compatibility, the value 0
702        # means horizontal, so we use "not extra" to get the parity right.
703        return RadioButtonArray(varname, params, checked, not extra)
704    elif (kind == mm_cfg.String or kind == mm_cfg.Email or
705          kind == mm_cfg.Host or kind == mm_cfg.Number):
706        return TextBox(varname, value, params)
707    elif kind == mm_cfg.Text:
708        if params:
709            r, c = params
710        else:
711            r, c = None, None
712        return TextArea(varname, value or '', r, c)
713    elif kind in (mm_cfg.EmailList, mm_cfg.EmailListEx):
714        if params:
715            r, c = params
716        else:
717            r, c = None, None
718        res = NL.join(value)
719        return TextArea(varname, res, r, c, wrap='off')
720    elif kind == mm_cfg.FileUpload:
721        # like a text area, but also with uploading
722        if params:
723            r, c = params
724        else:
725            r, c = None, None
726        container = Container()
727        container.AddItem(_('<em>Enter the text below, or...</em><br>'))
728        container.AddItem(TextArea(varname, value or '', r, c))
729        container.AddItem(_('<br><em>...specify a file to upload</em><br>'))
730        container.AddItem(FileUpload(varname+'_upload', r, c))
731        return container
732    elif kind == mm_cfg.Select:
733        if params:
734           values, legend, selected = params
735        else:
736           values = mlist.GetAvailableLanguages()
737           legend = map(_, map(Utils.GetLanguageDescr, values))
738           selected = values.index(mlist.preferred_language)
739        return SelectOptions(varname, values, legend, selected)
740    elif kind == mm_cfg.Topics:
741        # A complex and specialized widget type that allows for setting of a
742        # topic name, a mark button, a regexp text box, an "add after mark",
743        # and a delete button.  Yeesh!  params are ignored.
744        table = Table(border=0)
745        # This adds the html for the entry widget
746        def makebox(i, name, pattern, desc, empty=False, table=table):
747            deltag   = 'topic_delete_%02d' % i
748            boxtag   = 'topic_box_%02d' % i
749            reboxtag = 'topic_rebox_%02d' % i
750            desctag  = 'topic_desc_%02d' % i
751            wheretag = 'topic_where_%02d' % i
752            addtag   = 'topic_add_%02d' % i
753            newtag   = 'topic_new_%02d' % i
754            if empty:
755                table.AddRow([Center(Bold(_('Topic %(i)d'))),
756                              Hidden(newtag)])
757            else:
758                table.AddRow([Center(Bold(_('Topic %(i)d'))),
759                              SubmitButton(deltag, _('Delete'))])
760            table.AddRow([Label(_('Topic name:')),
761                          TextBox(boxtag, value=name, size=30)])
762            table.AddRow([Label(_('Regexp:')),
763                          TextArea(reboxtag, text=pattern,
764                                   rows=4, cols=30, wrap='off')])
765            table.AddRow([Label(_('Description:')),
766                          TextArea(desctag, text=desc,
767                                   rows=4, cols=30, wrap='soft')])
768            if not empty:
769                table.AddRow([SubmitButton(addtag, _('Add new item...')),
770                              SelectOptions(wheretag, ('before', 'after'),
771                                            (_('...before this one.'),
772                                             _('...after this one.')),
773                                            selected=1),
774                              ])
775            table.AddRow(['<hr>'])
776            table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
777        # Now for each element in the existing data, create a widget
778        i = 1
779        data = getattr(mlist, varname)
780        for name, pattern, desc, empty in data:
781            makebox(i, name, pattern, desc, empty)
782            i += 1
783        # Add one more non-deleteable widget as the first blank entry, but
784        # only if there are no real entries.
785        if i == 1:
786            makebox(i, '', '', '', empty=True)
787        return table
788    elif kind == mm_cfg.HeaderFilter:
789        # A complex and specialized widget type that allows for setting of a
790        # spam filter rule including, a mark button, a regexp text box, an
791        # "add after mark", up and down buttons, and a delete button.  Yeesh!
792        # params are ignored.
793        table = Table(border=0)
794        # This adds the html for the entry widget
795        def makebox(i, pattern, action, empty=False, table=table):
796            deltag    = 'hdrfilter_delete_%02d' % i
797            reboxtag  = 'hdrfilter_rebox_%02d' % i
798            actiontag = 'hdrfilter_action_%02d' % i
799            wheretag  = 'hdrfilter_where_%02d' % i
800            addtag    = 'hdrfilter_add_%02d' % i
801            newtag    = 'hdrfilter_new_%02d' % i
802            uptag     = 'hdrfilter_up_%02d' % i
803            downtag   = 'hdrfilter_down_%02d' % i
804            if empty:
805                table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
806                              Hidden(newtag)])
807            else:
808                table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))),
809                              SubmitButton(deltag, _('Delete'))])
810            table.AddRow([Label(_('Spam Filter Regexp:')),
811                          TextArea(reboxtag, text=pattern,
812                                   rows=4, cols=30, wrap='off')])
813            values = [mm_cfg.DEFER, mm_cfg.HOLD, mm_cfg.REJECT,
814                      mm_cfg.DISCARD, mm_cfg.ACCEPT]
815            try:
816                checked = values.index(action)
817            except ValueError:
818                checked = 0
819            radio = RadioButtonArray(
820                actiontag,
821                (_('Defer'), _('Hold'), _('Reject'),
822                 _('Discard'), _('Accept')),
823                values=values,
824                checked=checked).Format()
825            table.AddRow([Label(_('Action:')), radio])
826            if not empty:
827                table.AddRow([SubmitButton(addtag, _('Add new item...')),
828                              SelectOptions(wheretag, ('before', 'after'),
829                                            (_('...before this one.'),
830                                             _('...after this one.')),
831                                            selected=1),
832                              ])
833                # BAW: IWBNI we could disable the up and down buttons for the
834                # first and last item respectively, but it's not easy to know
835                # which is the last item, so let's not worry about that for
836                # now.
837                table.AddRow([SubmitButton(uptag, _('Move rule up')),
838                              SubmitButton(downtag, _('Move rule down'))])
839            table.AddRow(['<hr>'])
840            table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
841        # Now for each element in the existing data, create a widget
842        i = 1
843        data = getattr(mlist, varname)
844        for pattern, action, empty in data:
845            makebox(i, pattern, action, empty)
846            i += 1
847        # Add one more non-deleteable widget as the first blank entry, but
848        # only if there are no real entries.
849        if i == 1:
850            makebox(i, '', mm_cfg.DEFER, empty=True)
851        return table
852    elif kind == mm_cfg.Checkbox:
853        return CheckBoxArray(varname, *params)
854    else:
855        assert 0, 'Bad gui widget type: %s' % kind
856
857
858
859def get_item_gui_description(mlist, category, subcat,
860                             varname, descr, elaboration, detailsp):
861    # Return the item's description, with link to details.
862    #
863    # Details are not included if this is a VARHELP page, because that /is/
864    # the details page!
865    if detailsp:
866        if subcat:
867            varhelp = '/?VARHELP=%s/%s/%s' % (category, subcat, varname)
868        else:
869            varhelp = '/?VARHELP=%s/%s' % (category, varname)
870        if descr == elaboration:
871            linktext = _('<br>(Edit <b>%(varname)s</b>)')
872        else:
873            linktext = _('<br>(Details for <b>%(varname)s</b>)')
874        link = Link(mlist.GetScriptURL('admin') + varhelp,
875                    linktext).Format()
876        text = Label('%s %s' % (descr, link)).Format()
877    else:
878        text = Label(descr).Format()
879    if varname[0] == '_':
880        text += Label(_('''<br><em><strong>Note:</strong>
881        setting this value performs an immediate action but does not modify
882        permanent state.</em>''')).Format()
883    return text
884
885
886
887def membership_options(mlist, subcat, cgidata, doc, form):
888    # Show the main stuff
889    adminurl = mlist.GetScriptURL('admin', absolute=1)
890    container = Container()
891    header = Table(width="100%")
892    # If we're in the list subcategory, show the membership list
893    if subcat == 'add':
894        header.AddRow([Center(Header(2, _('Mass Subscriptions')))])
895        header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
896                           bgcolor=mm_cfg.WEB_HEADER_COLOR)
897        container.AddItem(header)
898        mass_subscribe(mlist, container)
899        return container
900    if subcat == 'remove':
901        header.AddRow([Center(Header(2, _('Mass Removals')))])
902        header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
903                           bgcolor=mm_cfg.WEB_HEADER_COLOR)
904        container.AddItem(header)
905        mass_remove(mlist, container)
906        return container
907    if subcat == 'change':
908        header.AddRow([Center(Header(2, _('Address Change')))])
909        header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
910                           bgcolor=mm_cfg.WEB_HEADER_COLOR)
911        container.AddItem(header)
912        address_change(mlist, container)
913        return container
914    if subcat == 'sync':
915        header.AddRow([Center(Header(2, _('Sync Membership List')))])
916        header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
917                           bgcolor=mm_cfg.WEB_HEADER_COLOR)
918        container.AddItem(header)
919        mass_sync(mlist, container)
920        return container
921    # Otherwise...
922    header.AddRow([Center(Header(2, _('Membership List')))])
923    header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
924                       bgcolor=mm_cfg.WEB_HEADER_COLOR)
925    container.AddItem(header)
926    # Add a "search for member" button
927    table = Table(width='100%')
928    link = Link('https://docs.python.org/2/library/re.html'
929                '#regular-expression-syntax',
930                _('(help)')).Format()
931    table.AddRow([Label(_('Find member %(link)s:')),
932                  TextBox('findmember',
933                          value=cgidata.getfirst('findmember', '')),
934                  SubmitButton('findmember_btn', _('Search...'))])
935    container.AddItem(table)
936    container.AddItem('<hr><p>')
937    usertable = Table(width="90%", border='2')
938    # If there are more members than allowed by chunksize, then we split the
939    # membership up alphabetically.  Otherwise just display them all.
940    chunksz = mlist.admin_member_chunksize
941    # The email addresses had /better/ be ASCII, but might be encoded in the
942    # database as Unicodes.
943    all = [_m.encode() for _m in mlist.getMembers()]
944    all.sort(lambda x, y: cmp(x.lower(), y.lower()))
945    # See if the query has a regular expression
946    regexp = cgidata.getfirst('findmember', '').strip()
947    try:
948        regexp = regexp.decode(Utils.GetCharSet(mlist.preferred_language))
949    except UnicodeDecodeError:
950        # This is probably a non-ascii character and an English language
951        # (ascii) list.  Even if we didn't throw the UnicodeDecodeError,
952        # the input may have contained mnemonic or numeric HTML entites mixed
953        # with other characters.  Trying to grok the real meaning out of that
954        # is complex and error prone, so we don't try.
955        pass
956    if regexp:
957        try:
958            cre = re.compile(regexp, re.IGNORECASE)
959        except re.error:
960            doc.addError(_('Bad regular expression: ') + regexp)
961        else:
962            # BAW: There's got to be a more efficient way of doing this!
963            names = [mlist.getMemberName(s) or '' for s in all]
964            all = [a for n, a in zip(names, all)
965                   if cre.search(n) or cre.search(a)]
966    chunkindex = None
967    bucket = None
968    actionurl = None
969    if len(all) < chunksz:
970        members = all
971    else:
972        # Split them up alphabetically, and then split the alphabetical
973        # listing by chunks
974        buckets = {}
975        for addr in all:
976            members = buckets.setdefault(addr[0].lower(), [])
977            members.append(addr)
978        # Now figure out which bucket we want
979        bucket = None
980        qs = {}
981        # POST methods, even if their actions have a query string, don't get
982        # put into FieldStorage's keys :-(
983        qsenviron = os.environ.get('QUERY_STRING')
984        if qsenviron:
985            qs = cgi.parse_qs(qsenviron)
986            bucket = qs.get('letter', '0')[0].lower()
987        keys = buckets.keys()
988        keys.sort()
989        if not bucket or not buckets.has_key(bucket):
990            bucket = keys[0]
991        members = buckets[bucket]
992        action = adminurl + '/members?letter=%s' % bucket
993        if len(members) <= chunksz:
994            form.set_action(action)
995        else:
996            i, r = divmod(len(members), chunksz)
997            numchunks = i + (not not r * 1)
998            # Now chunk them up
999            chunkindex = 0
1000            if qs.has_key('chunk'):
1001                try:
1002                    chunkindex = int(qs['chunk'][0])
1003                except ValueError:
1004                    chunkindex = 0
1005                if chunkindex < 0 or chunkindex > numchunks:
1006                    chunkindex = 0
1007            members = members[chunkindex*chunksz:(chunkindex+1)*chunksz]
1008            # And set the action URL
1009            form.set_action(action + '&chunk=%s' % chunkindex)
1010    # So now members holds all the addresses we're going to display
1011    allcnt = len(all)
1012    if bucket:
1013        membercnt = len(members)
1014        usertable.AddRow([Center(Italic(_(
1015            '%(allcnt)s members total, %(membercnt)s shown')))])
1016    else:
1017        usertable.AddRow([Center(Italic(_('%(allcnt)s members total')))])
1018    usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
1019                          usertable.GetCurrentCellIndex(),
1020                          colspan=OPTCOLUMNS,
1021                          bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
1022    # Add the alphabetical links
1023    if bucket:
1024        cells = []
1025        for letter in keys:
1026            findfrag = ''
1027            if regexp:
1028                findfrag = '&findmember=' + urllib.quote(regexp)
1029            url = adminurl + '/members?letter=' + letter + findfrag
1030            if isinstance(url, unicode):
1031                url = url.encode(Utils.GetCharSet(mlist.preferred_language),
1032                                 errors='ignore')
1033            if letter == bucket:
1034                show = Bold('[%s]' % letter.upper()).Format()
1035            else:
1036                show = letter.upper()
1037            cells.append(Link(url, show).Format())
1038        joiner = '&nbsp;'*2 + '\n'
1039        usertable.AddRow([Center(joiner.join(cells))])
1040    usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
1041                          usertable.GetCurrentCellIndex(),
1042                          colspan=OPTCOLUMNS,
1043                          bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
1044    usertable.AddRow([Center(h) for h in (_('unsub'),
1045                                          _('member address<br>member name'),
1046                                          _('mod'), _('hide'),
1047                                          _('nomail<br>[reason]'),
1048                                          _('ack'), _('not metoo'),
1049                                          _('nodupes'),
1050                                          _('digest'), _('plain'),
1051                                          _('language'))])
1052    rowindex = usertable.GetCurrentRowIndex()
1053    for i in range(OPTCOLUMNS):
1054        usertable.AddCellInfo(rowindex, i, bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
1055    # Find the longest name in the list
1056    longest = 0
1057    if members:
1058        names = filter(None, [mlist.getMemberName(s) for s in members])
1059        # Make the name field at least as long as the longest email address
1060        longest = max([len(s) for s in names + members])
1061    # Abbreviations for delivery status details
1062    ds_abbrevs = {MemberAdaptor.UNKNOWN : _('?'),
1063                  MemberAdaptor.BYUSER  : _('U'),
1064                  MemberAdaptor.BYADMIN : _('A'),
1065                  MemberAdaptor.BYBOUNCE: _('B'),
1066                  }
1067    # Now populate the rows
1068    for addr in members:
1069        qaddr = urllib.quote(addr)
1070        link = Link(mlist.GetOptionsURL(addr, obscure=1),
1071                    mlist.getMemberCPAddress(addr))
1072        fullname = Utils.uncanonstr(mlist.getMemberName(addr),
1073                                    mlist.preferred_language)
1074        name = TextBox(qaddr + '_realname', fullname, size=longest).Format()
1075        cells = [Center(CheckBox(qaddr + '_unsub', 'off', 0).Format()
1076                        + '<div class="hidden">' + _('unsub') + '</div>'),
1077                 link.Format() + '<br>' +
1078                 name +
1079                 Hidden('user', qaddr).Format(),
1080                 ]
1081        # Do the `mod' option
1082        if mlist.getMemberOption(addr, mm_cfg.Moderate):
1083            value = 'on'
1084            checked = 1
1085        else:
1086            value = 'off'
1087            checked = 0
1088        box = CheckBox('%s_mod' % qaddr, value, checked)
1089        cells.append(Center(box.Format()
1090            + '<div class="hidden">' + _('mod') + '</div>'))
1091        # Kluge, get these translated.
1092        (_('hide'), _('nomail'), _('ack'), _('notmetoo'), _('nodupes'))
1093        for opt in ('hide', 'nomail', 'ack', 'notmetoo', 'nodupes'):
1094            extra = '<div class="hidden">' + _(opt) + '</div>'
1095            if opt == 'nomail':
1096                status = mlist.getDeliveryStatus(addr)
1097                if status == MemberAdaptor.ENABLED:
1098                    value = 'off'
1099                    checked = 0
1100                else:
1101                    value = 'on'
1102                    checked = 1
1103                    extra = '[%s]' % ds_abbrevs[status] + extra
1104            elif mlist.getMemberOption(addr, mm_cfg.OPTINFO[opt]):
1105                value = 'on'
1106                checked = 1
1107            else:
1108                value = 'off'
1109                checked = 0
1110            box = CheckBox('%s_%s' % (qaddr, opt), value, checked)
1111            cells.append(Center(box.Format() + extra))
1112        # This code is less efficient than the original which did a has_key on
1113        # the underlying dictionary attribute.  This version is slower and
1114        # less memory efficient.  It points to a new MemberAdaptor interface
1115        # method.
1116        extra = '<div class="hidden">' + _('digest') + '</div>'
1117        if addr in mlist.getRegularMemberKeys():
1118            cells.append(Center(CheckBox(qaddr + '_digest', 'off', 0).Format()
1119                                + extra))
1120        else:
1121            cells.append(Center(CheckBox(qaddr + '_digest', 'on', 1).Format()
1122                                + extra))
1123        if mlist.getMemberOption(addr, mm_cfg.OPTINFO['plain']):
1124            value = 'on'
1125            checked = 1
1126        else:
1127            value = 'off'
1128            checked = 0
1129        cells.append(Center(CheckBox(
1130                            '%s_plain' % qaddr, value, checked).Format()
1131                            + '<div class="hidden">' + _('plain') + '</div>'))
1132        # User's preferred language
1133        langpref = mlist.getMemberLanguage(addr)
1134        langs = mlist.GetAvailableLanguages()
1135        langdescs = [_(Utils.GetLanguageDescr(lang)) for lang in langs]
1136        try:
1137            selected = langs.index(langpref)
1138        except ValueError:
1139            selected = 0
1140        cells.append(Center(SelectOptions(qaddr + '_language', langs,
1141                                          langdescs, selected)).Format())
1142        usertable.AddRow(cells)
1143    # Add the usertable and a legend
1144    legend = UnorderedList()
1145    legend.AddItem(
1146        _('<b>unsub</b> -- Click on this to unsubscribe the member.'))
1147    legend.AddItem(
1148        _("""<b>mod</b> -- The user's personal moderation flag.  If this is
1149        set, postings from them will be moderated, otherwise they will be
1150        approved."""))
1151    legend.AddItem(
1152        _("""<b>hide</b> -- Is the member's address concealed on
1153        the list of subscribers?"""))
1154    legend.AddItem(_(
1155        """<b>nomail</b> -- Is delivery to the member disabled?  If so, an
1156        abbreviation will be given describing the reason for the disabled
1157        delivery:
1158            <ul><li><b>U</b> -- Delivery was disabled by the user via their
1159                    personal options page.
1160                <li><b>A</b> -- Delivery was disabled by the list
1161                    administrators.
1162                <li><b>B</b> -- Delivery was disabled by the system due to
1163                    excessive bouncing from the member's address.
1164                <li><b>?</b> -- The reason for disabled delivery isn't known.
1165                    This is the case for all memberships which were disabled
1166                    in older versions of Mailman.
1167            </ul>"""))
1168    legend.AddItem(
1169        _('''<b>ack</b> -- Does the member get acknowledgements of their
1170        posts?'''))
1171    legend.AddItem(
1172        _('''<b>not metoo</b> -- Does the member want to avoid copies of their
1173        own postings?'''))
1174    legend.AddItem(
1175        _('''<b>nodupes</b> -- Does the member want to avoid duplicates of the
1176        same message?'''))
1177    legend.AddItem(
1178        _('''<b>digest</b> -- Does the member get messages in digests?
1179        (otherwise, individual messages)'''))
1180    legend.AddItem(
1181        _('''<b>plain</b> -- If getting digests, does the member get plain
1182        text digests?  (otherwise, MIME)'''))
1183    legend.AddItem(_("<b>language</b> -- Language preferred by the user"))
1184    addlegend = ''
1185    parsedqs = 0
1186    qsenviron = os.environ.get('QUERY_STRING')
1187    if qsenviron:
1188        qs = cgi.parse_qs(qsenviron).get('legend')
1189        if qs and isinstance(qs, ListType):
1190            qs = qs[0]
1191        if qs == 'yes':
1192            addlegend = 'legend=yes&'
1193    if addlegend:
1194        container.AddItem(legend.Format() + '<p>')
1195        container.AddItem(
1196            Link(adminurl + '/members/list',
1197                 _('Click here to hide the legend for this table.')))
1198    else:
1199        container.AddItem(
1200            Link(adminurl + '/members/list?legend=yes',
1201                 _('Click here to include the legend for this table.')))
1202    container.AddItem(Center(usertable))
1203
1204    # There may be additional chunks
1205    if chunkindex is not None:
1206        buttons = []
1207        url = adminurl + '/members?%sletter=%s&' % (addlegend, bucket)
1208        footer = _('''<p><em>To view more members, click on the appropriate
1209        range listed below:</em>''')
1210        chunkmembers = buckets[bucket]
1211        last = len(chunkmembers)
1212        for i in range(numchunks):
1213            if i == chunkindex:
1214                continue
1215            start = chunkmembers[i*chunksz]
1216            end = chunkmembers[min((i+1)*chunksz, last)-1]
1217            thisurl = url + 'chunk=%d' % i + findfrag
1218            if isinstance(thisurl, unicode):
1219                thisurl = thisurl.encode(
1220                                 Utils.GetCharSet(mlist.preferred_language),
1221                                 errors='ignore')
1222            link = Link(thisurl, _('from %(start)s to %(end)s'))
1223            buttons.append(link)
1224        buttons = UnorderedList(*buttons)
1225        container.AddItem(footer + buttons.Format() + '<p>')
1226    return container
1227
1228
1229
1230def mass_subscribe(mlist, container):
1231    # MASS SUBSCRIBE
1232    GREY = mm_cfg.WEB_ADMINITEM_COLOR
1233    table = Table(width='90%')
1234    table.AddRow([
1235        Label(_('Subscribe these users now or invite them?')),
1236        RadioButtonArray('subscribe_or_invite',
1237                         (_('Subscribe'), _('Invite')),
1238                         mm_cfg.DEFAULT_SUBSCRIBE_OR_INVITE,
1239                         values=(0, 1))
1240        ])
1241    table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1242    table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1243    table.AddRow([
1244        Label(_('Send welcome messages to new subscribers?')),
1245        RadioButtonArray('send_welcome_msg_to_this_batch',
1246                         (_('No'), _('Yes')),
1247                         mlist.send_welcome_msg,
1248                         values=(0, 1))
1249        ])
1250    table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1251    table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1252    table.AddRow([
1253        Label(_('Send notifications of new subscriptions to the list owner?')),
1254        RadioButtonArray('send_notifications_to_list_owner',
1255                         (_('No'), _('Yes')),
1256                         mlist.admin_notify_mchanges,
1257                         values=(0,1))
1258        ])
1259    table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1260    table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1261    table.AddRow([Italic(_('Enter one address per line below...'))])
1262    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1263    table.AddRow([Center(TextArea(name='subscribees',
1264                                  rows=10, cols='70%', wrap=None))])
1265    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1266    table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
1267                  FileUpload('subscribees_upload', cols='50')])
1268    container.AddItem(Center(table))
1269    # Invitation text
1270    table.AddRow(['&nbsp;', '&nbsp;'])
1271    table.AddRow([Italic(_("""Below, enter additional text to be added to the
1272    top of your invitation or the subscription notification.  Include at least
1273    one blank line at the end..."""))])
1274    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1275    table.AddRow([Center(TextArea(name='invitation',
1276                                  rows=10, cols='70%', wrap=None))])
1277    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1278
1279
1280
1281def mass_remove(mlist, container):
1282    # MASS UNSUBSCRIBE
1283    GREY = mm_cfg.WEB_ADMINITEM_COLOR
1284    table = Table(width='90%')
1285    table.AddRow([
1286        Label(_('Send unsubscription acknowledgement to the user?')),
1287        RadioButtonArray('send_unsub_ack_to_this_batch',
1288                         (_('No'), _('Yes')),
1289                         0, values=(0, 1))
1290        ])
1291    table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1292    table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1293    table.AddRow([
1294        Label(_('Send notifications to the list owner?')),
1295        RadioButtonArray('send_unsub_notifications_to_list_owner',
1296                         (_('No'), _('Yes')),
1297                         mlist.admin_notify_mchanges,
1298                         values=(0, 1))
1299        ])
1300    table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1301    table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1302    table.AddRow([Italic(_('Enter one address per line below...'))])
1303    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1304    table.AddRow([Center(TextArea(name='unsubscribees',
1305                                  rows=10, cols='70%', wrap=None))])
1306    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1307    table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
1308                  FileUpload('unsubscribees_upload', cols='50')])
1309    container.AddItem(Center(table))
1310
1311
1312
1313def address_change(mlist, container):
1314    # ADDRESS CHANGE
1315    GREY = mm_cfg.WEB_ADMINITEM_COLOR
1316    table = Table(width='90%')
1317    table.AddRow([Italic(_("""To change a list member's address, enter the
1318    member's current and new addresses below. Use the check boxes to send
1319    notice of the change to the old and/or new address(es)."""))])
1320    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=3)
1321    table.AddRow([
1322        Label(_("Member's current address")),
1323        TextBox(name='change_from'),
1324        CheckBox('notice_old', 'yes', 0).Format() +
1325            '&nbsp;' +
1326            _('Send notice')
1327        ])
1328    table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1329    table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1330    table.AddCellInfo(table.GetCurrentRowIndex(), 2, bgcolor=GREY)
1331    table.AddRow([
1332        Label(_('Address to change to')),
1333        TextBox(name='change_to'),
1334        CheckBox('notice_new', 'yes', 0).Format() +
1335            '&nbsp;' +
1336            _('Send notice')
1337        ])
1338    table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
1339    table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
1340    table.AddCellInfo(table.GetCurrentRowIndex(), 2, bgcolor=GREY)
1341    container.AddItem(Center(table))
1342
1343
1344
1345def mass_sync(mlist, container):
1346    # MASS SYNC
1347    table = Table(width='90%')
1348    table.AddRow([Italic(_('Enter one address per line below...'))])
1349    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1350    table.AddRow([Center(TextArea(name='memberlist',
1351                                  rows=10, cols='70%', wrap=None))])
1352    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1353    table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
1354                  FileUpload('memberlist_upload', cols='50')])
1355    container.AddItem(Center(table))
1356
1357
1358
1359def password_inputs(mlist):
1360    adminurl = mlist.GetScriptURL('admin', absolute=1)
1361    table = Table(cellspacing=3, cellpadding=4)
1362    table.AddRow([Center(Header(2, _('Change list ownership passwords')))])
1363    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
1364                      bgcolor=mm_cfg.WEB_HEADER_COLOR)
1365    table.AddRow([_("""\
1366The <em>list administrators</em> are the people who have ultimate control over
1367all parameters of this mailing list.  They are able to change any list
1368configuration variable available through these administration web pages.
1369
1370<p>The <em>list moderators</em> have more limited permissions; they are not
1371able to change any list configuration variable, but they are allowed to tend
1372to pending administration requests, including approving or rejecting held
1373subscription requests, and disposing of held postings.  Of course, the
1374<em>list administrators</em> can also tend to pending requests.
1375
1376<p>In order to split the list ownership duties into administrators and
1377moderators, you must set a separate moderator password in the fields below,
1378and also provide the email addresses of the list moderators in the
1379<a href="%(adminurl)s/general">general options section</a>.""")])
1380    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1381    # Set up the admin password table on the left
1382    atable = Table(border=0, cellspacing=3, cellpadding=4,
1383                   bgcolor=mm_cfg.WEB_ADMINPW_COLOR)
1384    atable.AddRow([Label(_('Enter new administrator password:')),
1385                   PasswordBox('newpw', size=20)])
1386    atable.AddRow([Label(_('Confirm administrator password:')),
1387                   PasswordBox('confirmpw', size=20)])
1388    # Set up the moderator password table on the right
1389    mtable = Table(border=0, cellspacing=3, cellpadding=4,
1390                   bgcolor=mm_cfg.WEB_ADMINPW_COLOR)
1391    mtable.AddRow([Label(_('Enter new moderator password:')),
1392                   PasswordBox('newmodpw', size=20)])
1393    mtable.AddRow([Label(_('Confirm moderator password:')),
1394                   PasswordBox('confirmmodpw', size=20)])
1395    # Add these tables to the overall password table
1396    table.AddRow([atable, mtable])
1397    table.AddRow([_("""\
1398In addition to the above passwords you may specify a password for
1399pre-approving posts to the list. Either of the above two passwords can
1400be used in an Approved: header or first body line pseudo-header to
1401pre-approve a post that would otherwise be held for moderation. In
1402addition, the password below, if set, can be used for that purpose and
1403no other.""")])
1404    table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
1405    # Set up the post password table
1406    ptable = Table(border=0, cellspacing=3, cellpadding=4,
1407                   bgcolor=mm_cfg.WEB_ADMINPW_COLOR)
1408    ptable.AddRow([Label(_('Enter new poster password:')),
1409                   PasswordBox('newpostpw', size=20)])
1410    ptable.AddRow([Label(_('Confirm poster password:')),
1411                   PasswordBox('confirmpostpw', size=20)])
1412    table.AddRow([ptable])
1413    return table
1414
1415
1416
1417def submit_button(name='submit'):
1418    table = Table(border=0, cellspacing=0, cellpadding=2)
1419    table.AddRow([Bold(SubmitButton(name, _('Submit Your Changes')))])
1420    table.AddCellInfo(table.GetCurrentRowIndex(), 0, align='middle')
1421    return table
1422
1423
1424
1425def change_options(mlist, category, subcat, cgidata, doc):
1426    global _
1427    def safeint(formvar, defaultval=None):
1428        try:
1429            return int(cgidata.getfirst(formvar))
1430        except (ValueError, TypeError):
1431            return defaultval
1432    confirmed = 0
1433    # Handle changes to the list moderator password.  Do this before checking
1434    # the new admin password, since the latter will force a reauthentication.
1435    new = cgidata.getfirst('newmodpw', '').strip()
1436    confirm = cgidata.getfirst('confirmmodpw', '').strip()
1437    if new or confirm:
1438        if new == confirm:
1439            mlist.mod_password = sha_new(new).hexdigest()
1440            # No re-authentication necessary because the moderator's
1441            # password doesn't get you into these pages.
1442        else:
1443            doc.addError(_('Moderator passwords did not match'))
1444    # Handle changes to the list poster password.  Do this before checking
1445    # the new admin password, since the latter will force a reauthentication.
1446    new = cgidata.getfirst('newpostpw', '').strip()
1447    confirm = cgidata.getfirst('confirmpostpw', '').strip()
1448    if new or confirm:
1449        if new == confirm:
1450            mlist.post_password = sha_new(new).hexdigest()
1451            # No re-authentication necessary because the poster's
1452            # password doesn't get you into these pages.
1453        else:
1454            doc.addError(_('Poster passwords did not match'))
1455    # Handle changes to the list administrator password
1456    new = cgidata.getfirst('newpw', '').strip()
1457    confirm = cgidata.getfirst('confirmpw', '').strip()
1458    if new or confirm:
1459        if new == confirm:
1460            mlist.password = sha_new(new).hexdigest()
1461            # Set new cookie
1462            print mlist.MakeCookie(mm_cfg.AuthListAdmin)
1463        else:
1464            doc.addError(_('Administrator passwords did not match'))
1465    # Give the individual gui item a chance to process the form data
1466    categories = mlist.GetConfigCategories()
1467    label, gui = categories[category]
1468    # BAW: We handle the membership page special... for now.
1469    if category <> 'members':
1470        gui.handleForm(mlist, category, subcat, cgidata, doc)
1471    # mass subscription, removal processing for members category
1472    subscribers = ''
1473    subscribers += cgidata.getfirst('subscribees', '')
1474    subscribers += cgidata.getfirst('subscribees_upload', '')
1475    if subscribers:
1476        entries = filter(None, [n.strip() for n in subscribers.splitlines()])
1477        send_welcome_msg = safeint('send_welcome_msg_to_this_batch',
1478                                   mlist.send_welcome_msg)
1479        send_admin_notif = safeint('send_notifications_to_list_owner',
1480                                   mlist.admin_notify_mchanges)
1481        # Default is to subscribe
1482        subscribe_or_invite = safeint('subscribe_or_invite', 0)
1483        invitation = cgidata.getfirst('invitation', '')
1484        digest = mlist.digest_is_default
1485        if not mlist.digestable:
1486            digest = 0
1487        if not mlist.nondigestable:
1488            digest = 1
1489        subscribe_errors = []
1490        subscribe_success = []
1491        # Now cruise through all the subscribees and do the deed.  BAW: we
1492        # should limit the number of "Successfully subscribed" status messages
1493        # we display.  Try uploading a file with 10k names -- it takes a while
1494        # to render the status page.
1495        for entry in entries:
1496            safeentry = Utils.websafe(entry)
1497            fullname, address = parseaddr(entry)
1498            # Canonicalize the full name
1499            fullname = Utils.canonstr(fullname, mlist.preferred_language)
1500            userdesc = UserDesc(address, fullname,
1501                                Utils.MakeRandomPassword(),
1502                                digest, mlist.preferred_language)
1503            try:
1504                if subscribe_or_invite:
1505                    if mlist.isMember(address):
1506                        raise Errors.MMAlreadyAMember
1507                    else:
1508                        mlist.InviteNewMember(userdesc, invitation)
1509                else:
1510                    _ = D_
1511                    whence = _('admin mass sub')
1512                    _ = i18n._
1513                    mlist.ApprovedAddMember(userdesc, send_welcome_msg,
1514                                            send_admin_notif, invitation,
1515                                            whence=whence)
1516            except Errors.MMAlreadyAMember:
1517                subscribe_errors.append((safeentry, _('Already a member')))
1518            except Errors.MMBadEmailError:
1519                if userdesc.address == '':
1520                    subscribe_errors.append((_('&lt;blank line&gt;'),
1521                                             _('Bad/Invalid email address')))
1522                else:
1523                    subscribe_errors.append((safeentry,
1524                                             _('Bad/Invalid email address')))
1525            except Errors.MMHostileAddress:
1526                subscribe_errors.append(
1527                    (safeentry, _('Hostile address (illegal characters)')))
1528            except Errors.MembershipIsBanned, pattern:
1529                subscribe_errors.append(
1530                    (safeentry, _('Banned address (matched %(pattern)s)')))
1531            else:
1532                member = Utils.uncanonstr(formataddr((fullname, address)))
1533                subscribe_success.append(Utils.websafe(member))
1534        if subscribe_success:
1535            if subscribe_or_invite:
1536                doc.AddItem(Header(5, _('Successfully invited:')))
1537            else:
1538                doc.AddItem(Header(5, _('Successfully subscribed:')))
1539            doc.AddItem(UnorderedList(*subscribe_success))
1540            doc.AddItem('<p>')
1541        if subscribe_errors:
1542            if subscribe_or_invite:
1543                doc.AddItem(Header(5, _('Error inviting:')))
1544            else:
1545                doc.AddItem(Header(5, _('Error subscribing:')))
1546            items = ['%s -- %s' % (x0, x1) for x0, x1 in subscribe_errors]
1547            doc.AddItem(UnorderedList(*items))
1548            doc.AddItem('<p>')
1549    # Unsubscriptions
1550    removals = ''
1551    if cgidata.has_key('unsubscribees'):
1552        removals += cgidata['unsubscribees'].value
1553    if cgidata.has_key('unsubscribees_upload') and \
1554           cgidata['unsubscribees_upload'].value:
1555        removals += cgidata['unsubscribees_upload'].value
1556    if removals:
1557        names = filter(None, [n.strip() for n in removals.splitlines()])
1558        send_unsub_notifications = safeint(
1559            'send_unsub_notifications_to_list_owner',
1560            mlist.admin_notify_mchanges)
1561        userack = safeint(
1562            'send_unsub_ack_to_this_batch',
1563            mlist.send_goodbye_msg)
1564        unsubscribe_errors = []
1565        unsubscribe_success = []
1566        for addr in names:
1567            try:
1568                _ = D_
1569                whence = _('admin mass unsub')
1570                _ = i18n._
1571                mlist.ApprovedDeleteMember(
1572                    addr, whence=whence,
1573                    admin_notif=send_unsub_notifications,
1574                    userack=userack)
1575                unsubscribe_success.append(Utils.websafe(addr))
1576            except Errors.NotAMemberError:
1577                unsubscribe_errors.append(Utils.websafe(addr))
1578        if unsubscribe_success:
1579            doc.AddItem(Header(5, _('Successfully Unsubscribed:')))
1580            doc.AddItem(UnorderedList(*unsubscribe_success))
1581            doc.AddItem('<p>')
1582        if unsubscribe_errors:
1583            doc.AddItem(Header(3, Bold(FontAttr(
1584                _('Cannot unsubscribe non-members:'),
1585                color='#ff0000', size='+2')).Format()))
1586            doc.AddItem(UnorderedList(*unsubscribe_errors))
1587            doc.AddItem('<p>')
1588    # Address Changes
1589    if cgidata.has_key('change_from'):
1590        change_from = cgidata.getfirst('change_from', '')
1591        change_to = cgidata.getfirst('change_to', '')
1592        schange_from = Utils.websafe(change_from)
1593        schange_to = Utils.websafe(change_to)
1594        success = False
1595        msg = None
1596        if not (change_from and change_to):
1597            msg = _('You must provide both current and new addresses.')
1598        elif change_from == change_to:
1599            msg = _('Current and new addresses must be different.')
1600        elif mlist.isMember(change_to):
1601            # ApprovedChangeMemberAddress will just delete the old address
1602            # and we don't want that here.
1603            msg = _('%(schange_to)s is already a list member.')
1604        else:
1605            try:
1606                Utils.ValidateEmail(change_to)
1607            except (Errors.MMBadEmailError, Errors.MMHostileAddress):
1608                msg = _('%(schange_to)s is not a valid email address.')
1609        if msg:
1610            doc.AddItem(Header(3, msg))
1611            doc.AddItem('<p>')
1612            return
1613        try:
1614            mlist.ApprovedChangeMemberAddress(change_from, change_to, False)
1615        except Errors.NotAMemberError:
1616            msg = _('%(schange_from)s is not a member')
1617        except Errors.MMAlreadyAMember:
1618            msg = _('%(schange_to)s is already a member')
1619        except Errors.MembershipIsBanned, pat:
1620            spat = Utils.websafe(str(pat))
1621            msg = _('%(schange_to)s matches banned pattern %(spat)s')
1622        else:
1623            msg = _('Address %(schange_from)s changed to %(schange_to)s')
1624            success = True
1625        doc.AddItem(Header(3, msg))
1626        lang = mlist.getMemberLanguage(change_to)
1627        otrans = i18n.get_translation()
1628        i18n.set_language(lang)
1629        list_name = mlist.getListAddress()
1630        text = Utils.wrap(_("""The member address %(change_from)s on the
1631%(list_name)s list has been changed to %(change_to)s.
1632"""))
1633        subject = _('%(list_name)s address change notice.')
1634        i18n.set_translation(otrans)
1635        if success and cgidata.getfirst('notice_old', '') == 'yes':
1636            # Send notice to old address.
1637            msg = Message.UserNotification(change_from,
1638                mlist.GetOwnerEmail(),
1639                text=text,
1640                subject=subject,
1641                lang=lang
1642                )
1643            msg.send(mlist)
1644            doc.AddItem(Header(3, _('Notification sent to %(schange_from)s.')))
1645        if success and cgidata.getfirst('notice_new', '') == 'yes':
1646            # Send notice to new address.
1647            msg = Message.UserNotification(change_to,
1648                mlist.GetOwnerEmail(),
1649                text=text,
1650                subject=subject,
1651                lang=lang
1652                )
1653            msg.send(mlist)
1654            doc.AddItem(Header(3, _('Notification sent to %(schange_to)s.')))
1655        doc.AddItem('<p>')
1656
1657    # sync operation
1658    memberlist = ''
1659    memberlist += cgidata.getvalue('memberlist', '')
1660    memberlist += cgidata.getvalue('memberlist_upload', '')
1661    if memberlist:
1662        # Browsers will convert special characters in the text box to HTML
1663        # entities. We need to fix those.
1664        def i_to_c(mo):
1665            # Convert a matched string of digits to the corresponding unicode.
1666            return unichr(int(mo.group(1)))
1667        def clean_input(x):
1668            # Strip leading/trailing whitespace and convert numeric HTML
1669            # entities.
1670            return re.sub(r'&#(\d+);', i_to_c, x.strip())
1671        entries = filter(None,
1672                         [clean_input(n) for n in memberlist.splitlines()])
1673        lc_addresses = [parseaddr(x)[1].lower() for x in entries
1674                        if parseaddr(x)[1]]
1675        subscribe_errors = []
1676        subscribe_success = []
1677        # First we add all the addresses that should be added to the list.
1678        for entry in entries:
1679            safeentry = Utils.websafe(entry)
1680            fullname, address = parseaddr(entry)
1681            if mlist.isMember(address):
1682                continue
1683            # Canonicalize the full name.
1684            fullname = Utils.canonstr(fullname, mlist.preferred_language)
1685            userdesc = UserDesc(address, fullname,
1686                                Utils.MakeRandomPassword(),
1687                                0, mlist.preferred_language)
1688            try:
1689                # Add a member if not yet member.
1690                    mlist.ApprovedAddMember(userdesc, 0, 0, 0,
1691                                            whence='admin sync members')
1692            except Errors.MMBadEmailError:
1693                if userdesc.address == '':
1694                    subscribe_errors.append((_('&lt;blank line&gt;'),
1695                                             _('Bad/Invalid email address')))
1696                else:
1697                    subscribe_errors.append((safeentry,
1698                                             _('Bad/Invalid email address')))
1699            except Errors.MMHostileAddress:
1700                subscribe_errors.append(
1701                    (safeentry, _('Hostile address (illegal characters)')))
1702            except Errors.MembershipIsBanned, pattern:
1703                subscribe_errors.append(
1704                    (safeentry, _('Banned address (matched %(pattern)s)')))
1705            else:
1706                member = Utils.uncanonstr(formataddr((fullname, address)))
1707                subscribe_success.append(Utils.websafe(member))
1708
1709        # Then we remove the addresses not in our list.
1710        unsubscribe_errors = []
1711        unsubscribe_success = []
1712
1713        for entry in mlist.getMembers():
1714            # If an entry is not found in the uploaded "entries" list, then
1715            # remove the member.
1716            if not(entry in lc_addresses):
1717                try:
1718                    mlist.ApprovedDeleteMember(entry, 0, 0)
1719                except Errors.NotAMemberError:
1720                    # This can happen if the address is illegal (i.e. can't be
1721                    # parsed by email.Utils.parseaddr()) but for legacy
1722                    # reasons is in the database.  Use a lower level remove to
1723                    # get rid of this member's entry
1724                    mlist.removeMember(entry)
1725                else:
1726                    unsubscribe_success.append(Utils.websafe(entry))
1727
1728        if subscribe_success:
1729            doc.AddItem(Header(5, _('Successfully subscribed:')))
1730            doc.AddItem(UnorderedList(*subscribe_success))
1731            doc.AddItem('<p>')
1732        if subscribe_errors:
1733            doc.AddItem(Header(5, _('Error subscribing:')))
1734            items = ['%s -- %s' % (x0, x1) for x0, x1 in subscribe_errors]
1735            doc.AddItem(UnorderedList(*items))
1736            doc.AddItem('<p>')
1737        if unsubscribe_success:
1738            doc.AddItem(Header(5, _('Successfully Unsubscribed:')))
1739            doc.AddItem(UnorderedList(*unsubscribe_success))
1740            doc.AddItem('<p>')
1741
1742    # See if this was a moderation bit operation
1743    if cgidata.has_key('allmodbit_btn'):
1744        val = safeint('allmodbit_val')
1745        if val not in (0, 1):
1746            doc.addError(_('Bad moderation flag value'))
1747        else:
1748            for member in mlist.getMembers():
1749                mlist.setMemberOption(member, mm_cfg.Moderate, val)
1750    # do the user options for members category
1751    if cgidata.has_key('setmemberopts_btn') and cgidata.has_key('user'):
1752        user = cgidata['user']
1753        if type(user) is ListType:
1754            users = []
1755            for ui in range(len(user)):
1756                users.append(urllib.unquote(user[ui].value))
1757        else:
1758            users = [urllib.unquote(user.value)]
1759        errors = []
1760        removes = []
1761        for user in users:
1762            quser = urllib.quote(user)
1763            if cgidata.has_key('%s_unsub' % quser):
1764                try:
1765                    _ = D_
1766                    whence=_('member mgt page')
1767                    _ = i18n._
1768                    mlist.ApprovedDeleteMember(user, whence=whence)
1769                    removes.append(user)
1770                except Errors.NotAMemberError:
1771                    errors.append((user, _('Not subscribed')))
1772                continue
1773            if not mlist.isMember(user):
1774                doc.addError(_('Ignoring changes to deleted member: %(user)s'),
1775                             tag=_('Warning: '))
1776                continue
1777            value = cgidata.has_key('%s_digest' % quser)
1778            try:
1779                mlist.setMemberOption(user, mm_cfg.Digests, value)
1780            except (Errors.AlreadyReceivingDigests,
1781                    Errors.AlreadyReceivingRegularDeliveries,
1782                    Errors.CantDigestError,
1783                    Errors.MustDigestError):
1784                # BAW: Hmm...
1785                pass
1786
1787            newname = cgidata.getfirst(quser+'_realname', '')
1788            newname = Utils.canonstr(newname, mlist.preferred_language)
1789            mlist.setMemberName(user, newname)
1790
1791            newlang = cgidata.getfirst(quser+'_language')
1792            oldlang = mlist.getMemberLanguage(user)
1793            if Utils.IsLanguage(newlang) and newlang <> oldlang:
1794                mlist.setMemberLanguage(user, newlang)
1795
1796            moderate = not not cgidata.getfirst(quser+'_mod')
1797            mlist.setMemberOption(user, mm_cfg.Moderate, moderate)
1798
1799            # Set the `nomail' flag, but only if the user isn't already
1800            # disabled (otherwise we might change BYUSER into BYADMIN).
1801            if cgidata.has_key('%s_nomail' % quser):
1802                if mlist.getDeliveryStatus(user) == MemberAdaptor.ENABLED:
1803                    mlist.setDeliveryStatus(user, MemberAdaptor.BYADMIN)
1804            else:
1805                mlist.setDeliveryStatus(user, MemberAdaptor.ENABLED)
1806            for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain'):
1807                opt_code = mm_cfg.OPTINFO[opt]
1808                if cgidata.has_key('%s_%s' % (quser, opt)):
1809                    mlist.setMemberOption(user, opt_code, 1)
1810                else:
1811                    mlist.setMemberOption(user, opt_code, 0)
1812        # Give some feedback on who's been removed
1813        if removes:
1814            doc.AddItem(Header(5, _('Successfully Removed:')))
1815            doc.AddItem(UnorderedList(*removes))
1816            doc.AddItem('<p>')
1817        if errors:
1818            doc.AddItem(Header(5, _("Error Unsubscribing:")))
1819            items = ['%s -- %s' % (x[0], x[1]) for x in errors]
1820            doc.AddItem(apply(UnorderedList, tuple((items))))
1821            doc.AddItem("<p>")
1822