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 process the pending-approval items for a list."""
19
20import sys
21import os
22import cgi
23import errno
24import signal
25import email
26import time
27from types import ListType
28from urllib import quote_plus, unquote_plus
29
30from Mailman import mm_cfg
31from Mailman import Utils
32from Mailman import MailList
33from Mailman import Errors
34from Mailman import Message
35from Mailman import i18n
36from Mailman.Handlers.Moderate import ModeratedMemberPost
37from Mailman.ListAdmin import HELDMSG
38from Mailman.ListAdmin import readMessage
39from Mailman.Cgi import Auth
40from Mailman.htmlformat import *
41from Mailman.Logging.Syslog import syslog
42from Mailman.CSRFcheck import csrf_check
43
44EMPTYSTRING = ''
45NL = '\n'
46
47# Set up i18n.  Until we know which list is being requested, we use the
48# server's default.
49_ = i18n._
50i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
51
52EXCERPT_HEIGHT = 10
53EXCERPT_WIDTH = 76
54SSENDER = mm_cfg.SSENDER
55SSENDERTIME = mm_cfg.SSENDERTIME
56STIME = mm_cfg.STIME
57if mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS in (SSENDERTIME, STIME):
58    ssort = mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS
59else:
60    ssort = SSENDER
61
62AUTH_CONTEXTS = (mm_cfg.AuthListModerator, mm_cfg.AuthListAdmin,
63                 mm_cfg.AuthSiteAdmin)
64
65
66
67def helds_by_skey(mlist, ssort=SSENDER):
68    heldmsgs = mlist.GetHeldMessageIds()
69    byskey = {}
70    for id in heldmsgs:
71        ptime = mlist.GetRecord(id)[0]
72        sender = mlist.GetRecord(id)[1]
73        if ssort in (SSENDER, SSENDERTIME):
74            skey = (0, sender)
75        else:
76            skey = (ptime, sender)
77        byskey.setdefault(skey, []).append((ptime, id))
78    # Sort groups by time
79    for k, v in byskey.items():
80        if len(v) > 1:
81            v.sort()
82            byskey[k] = v
83        if ssort == SSENDERTIME:
84            # Rekey with time
85            newkey = (v[0][0], k[1])
86            del byskey[k]
87            byskey[newkey] = v
88    return byskey
89
90
91def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3):
92    # We can't use a RadioButtonArray here because horizontal placement can be
93    # confusing to the user and vertical placement takes up too much
94    # real-estate.  This is a hack!
95    space = ' ' * spacing
96    btns = Table(cellspacing='5', cellpadding='0')
97    btns.AddRow([space + text + space for text in labels])
98    btns.AddRow([Center(RadioButton(btnname, value, default).Format()
99                     + '<div class=hidden>' + label + '</div>')
100                 for label, value, default in zip(labels, values, defaults)])
101    return btns
102
103
104
105def main():
106    global ssort
107    # Figure out which list is being requested
108    parts = Utils.GetPathPieces()
109    if not parts:
110        handle_no_list()
111        return
112
113    listname = parts[0].lower()
114    try:
115        mlist = MailList.MailList(listname, lock=0)
116    except Errors.MMListError, e:
117        # Avoid cross-site scripting attacks
118        safelistname = Utils.websafe(listname)
119        # Send this with a 404 status.
120        print 'Status: 404 Not Found'
121        handle_no_list(_('No such list <em>%(safelistname)s</em>'))
122        syslog('error', 'admindb: No such list "%s": %s\n', listname, e)
123        return
124
125    # Now that we know which list to use, set the system's language to it.
126    i18n.set_language(mlist.preferred_language)
127
128    # Make sure the user is authorized to see this page.
129    cgidata = cgi.FieldStorage(keep_blank_values=1)
130    try:
131        cgidata.getfirst('adminpw', '')
132    except TypeError:
133        # Someone crafted a POST with a bad Content-Type:.
134        doc = Document()
135        doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
136        doc.AddItem(Header(2, _("Error")))
137        doc.AddItem(Bold(_('Invalid options to CGI script.')))
138        # Send this with a 400 status.
139        print 'Status: 400 Bad Request'
140        print doc.Format()
141        return
142
143    # CSRF check
144    safe_params = ['adminpw', 'admlogin', 'msgid', 'sender', 'details']
145    params = cgidata.keys()
146    if set(params) - set(safe_params):
147        csrf_checked = csrf_check(mlist, cgidata.getfirst('csrf_token'),
148                                  'admindb')
149    else:
150        csrf_checked = True
151    # if password is present, void cookie to force password authentication.
152    if cgidata.getfirst('adminpw'):
153        os.environ['HTTP_COOKIE'] = ''
154        csrf_checked = True
155
156    if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
157                                  mm_cfg.AuthListModerator,
158                                  mm_cfg.AuthSiteAdmin),
159                                 cgidata.getfirst('adminpw', '')):
160        if cgidata.has_key('adminpw'):
161            # This is a re-authorization attempt
162            msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
163            remote = os.environ.get('HTTP_FORWARDED_FOR',
164                     os.environ.get('HTTP_X_FORWARDED_FOR',
165                     os.environ.get('REMOTE_ADDR',
166                                    'unidentified origin')))
167            syslog('security',
168                   'Authorization failed (admindb): list=%s: remote=%s',
169                   listname, remote)
170        else:
171            msg = ''
172        Auth.loginpage(mlist, 'admindb', msg=msg)
173        return
174
175    # Add logout function. Note that admindb may be accessed with
176    # site-wide admin, moderator and list admin privileges.
177    # site admin may have site or admin cookie. (or both?)
178    # See if this is a logout request
179    if len(parts) >= 2 and parts[1] == 'logout':
180        if mlist.AuthContextInfo(mm_cfg.AuthSiteAdmin)[0] == 'site':
181            print mlist.ZapCookie(mm_cfg.AuthSiteAdmin)
182        if mlist.AuthContextInfo(mm_cfg.AuthListModerator)[0]:
183            print mlist.ZapCookie(mm_cfg.AuthListModerator)
184        print mlist.ZapCookie(mm_cfg.AuthListAdmin)
185        Auth.loginpage(mlist, 'admindb', frontpage=1)
186        return
187
188    # Set up the results document
189    doc = Document()
190    doc.set_language(mlist.preferred_language)
191
192    # See if we're requesting all the messages for a particular sender, or if
193    # we want a specific held message.
194    sender = None
195    msgid = None
196    details = None
197    envar = os.environ.get('QUERY_STRING')
198    if envar:
199        # POST methods, even if their actions have a query string, don't get
200        # put into FieldStorage's keys :-(
201        qs = cgi.parse_qs(envar).get('sender')
202        if qs and type(qs) == ListType:
203            sender = qs[0]
204        qs = cgi.parse_qs(envar).get('msgid')
205        if qs and type(qs) == ListType:
206            msgid = qs[0]
207        qs = cgi.parse_qs(envar).get('details')
208        if qs and type(qs) == ListType:
209            details = qs[0]
210
211    # We need a signal handler to catch the SIGTERM that can come from Apache
212    # when the user hits the browser's STOP button.  See the comment in
213    # admin.py for details.
214    #
215    # BAW: Strictly speaking, the list should not need to be locked just to
216    # read the request database.  However the request database asserts that
217    # the list is locked in order to load it and it's not worth complicating
218    # that logic.
219    def sigterm_handler(signum, frame, mlist=mlist):
220        # Make sure the list gets unlocked...
221        mlist.Unlock()
222        # ...and ensure we exit, otherwise race conditions could cause us to
223        # enter MailList.Save() while we're in the unlocked state, and that
224        # could be bad!
225        sys.exit(0)
226
227    mlist.Lock()
228    try:
229        # Install the emergency shutdown signal handler
230        signal.signal(signal.SIGTERM, sigterm_handler)
231
232        realname = mlist.real_name
233        if not cgidata.keys() or cgidata.has_key('admlogin'):
234            # If this is not a form submission (i.e. there are no keys in the
235            # form) or it's a login, then we don't need to do much special.
236            doc.SetTitle(_('%(realname)s Administrative Database'))
237        elif not details:
238            # This is a form submission
239            doc.SetTitle(_('%(realname)s Administrative Database Results'))
240            if csrf_checked:
241                process_form(mlist, doc, cgidata)
242            else:
243                doc.addError(
244                    _('The form lifetime has expired. (request forgery check)'))
245        # Now print the results and we're done.  Short circuit for when there
246        # are no pending requests, but be sure to save the results!
247        admindburl = mlist.GetScriptURL('admindb', absolute=1)
248        if not mlist.NumRequestsPending():
249            title = _('%(realname)s Administrative Database')
250            doc.SetTitle(title)
251            doc.AddItem(Header(2, title))
252            doc.AddItem(_('There are no pending requests.'))
253            doc.AddItem(' ')
254            doc.AddItem(Link(admindburl,
255                             _('Click here to reload this page.')))
256            # Put 'Logout' link before the footer
257            doc.AddItem('\n<div align="right"><font size="+2">')
258            doc.AddItem(Link('%s/logout' % admindburl,
259                '<b>%s</b>' % _('Logout')))
260            doc.AddItem('</font></div>\n')
261            doc.AddItem(mlist.GetMailmanFooter())
262            print doc.Format()
263            mlist.Save()
264            return
265
266        form = Form(admindburl, mlist=mlist, contexts=AUTH_CONTEXTS)
267        # Add the instructions template
268        if details == 'instructions':
269            doc.AddItem(Header(
270                2, _('Detailed instructions for the administrative database')))
271        else:
272            doc.AddItem(Header(
273                2,
274                _('Administrative requests for mailing list:')
275                + ' <em>%s</em>' % mlist.real_name))
276        if details <> 'instructions':
277            form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
278        nomessages = not mlist.GetHeldMessageIds()
279        if not (details or sender or msgid or nomessages):
280            form.AddItem(Center(
281                '<label>' +
282                CheckBox('discardalldefersp', 0).Format() +
283                '&nbsp;' +
284                _('Discard all messages marked <em>Defer</em>') +
285                '</label>'
286                ))
287        # Add a link back to the overview, if we're not viewing the overview!
288        adminurl = mlist.GetScriptURL('admin', absolute=1)
289        d = {'listname'  : mlist.real_name,
290             'detailsurl': admindburl + '?details=instructions',
291             'summaryurl': admindburl,
292             'viewallurl': admindburl + '?details=all',
293             'adminurl'  : adminurl,
294             'filterurl' : adminurl + '/privacy/sender',
295             }
296        addform = 1
297        if sender:
298            esender = Utils.websafe(sender)
299            d['description'] = _("all of %(esender)s's held messages.")
300            doc.AddItem(Utils.maketext('admindbpreamble.html', d,
301                                       raw=1, mlist=mlist))
302            show_sender_requests(mlist, form, sender)
303        elif msgid:
304            d['description'] = _('a single held message.')
305            doc.AddItem(Utils.maketext('admindbpreamble.html', d,
306                                       raw=1, mlist=mlist))
307            show_message_requests(mlist, form, msgid)
308        elif details == 'all':
309            d['description'] = _('all held messages.')
310            doc.AddItem(Utils.maketext('admindbpreamble.html', d,
311                                       raw=1, mlist=mlist))
312            show_detailed_requests(mlist, form)
313        elif details == 'instructions':
314            doc.AddItem(Utils.maketext('admindbdetails.html', d,
315                                       raw=1, mlist=mlist))
316            addform = 0
317        else:
318            # Show a summary of all requests
319            doc.AddItem(Utils.maketext('admindbsummary.html', d,
320                                       raw=1, mlist=mlist))
321            num = show_pending_subs(mlist, form)
322            num += show_pending_unsubs(mlist, form)
323            num += show_helds_overview(mlist, form, ssort)
324            addform = num > 0
325        # Finish up the document, adding buttons to the form
326        if addform:
327            doc.AddItem(form)
328            form.AddItem('<hr>')
329            if not (details or sender or msgid or nomessages):
330                form.AddItem(Center(
331                    '<label>' +
332                    CheckBox('discardalldefersp', 0).Format() +
333                    '&nbsp;' +
334                    _('Discard all messages marked <em>Defer</em>') +
335                    '</label>'
336                    ))
337            form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
338        # Put 'Logout' link before the footer
339        doc.AddItem('\n<div align="right"><font size="+2">')
340        doc.AddItem(Link('%s/logout' % admindburl,
341            '<b>%s</b>' % _('Logout')))
342        doc.AddItem('</font></div>\n')
343        doc.AddItem(mlist.GetMailmanFooter())
344        print doc.Format()
345        # Commit all changes
346        mlist.Save()
347    finally:
348        mlist.Unlock()
349
350
351
352def handle_no_list(msg=''):
353    # Print something useful if no list was given.
354    doc = Document()
355    doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
356
357    header = _('Mailman Administrative Database Error')
358    doc.SetTitle(header)
359    doc.AddItem(Header(2, header))
360    doc.AddItem(msg)
361    url = Utils.ScriptURL('admin', absolute=1)
362    link = Link(url, _('list of available mailing lists.')).Format()
363    doc.AddItem(_('You must specify a list name.  Here is the %(link)s'))
364    doc.AddItem('<hr>')
365    doc.AddItem(MailmanLogo())
366    print doc.Format()
367
368
369
370def show_pending_subs(mlist, form):
371    # Add the subscription request section
372    pendingsubs = mlist.GetSubscriptionIds()
373    if not pendingsubs:
374        return 0
375    form.AddItem('<hr>')
376    form.AddItem(Center(Header(2, _('Subscription Requests'))))
377    table = Table(border=2)
378    table.AddRow([Center(Bold(_('Address/name/time'))),
379                  Center(Bold(_('Your decision'))),
380                  Center(Bold(_('Reason for refusal')))
381                  ])
382    # Alphabetical order by email address
383    byaddrs = {}
384    for id in pendingsubs:
385        addr = mlist.GetRecord(id)[1]
386        byaddrs.setdefault(addr, []).append(id)
387    addrs = byaddrs.items()
388    addrs.sort()
389    num = 0
390    for addr, ids in addrs:
391        # Eliminate duplicates.
392        # The list ws returned sorted ascending.  Keep the last.
393        for id in ids[:-1]:
394            mlist.HandleRequest(id, mm_cfg.DISCARD)
395        id = ids[-1]
396        stime, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
397        fullname = Utils.uncanonstr(fullname, mlist.preferred_language)
398        displaytime = time.ctime(stime)
399        radio = RadioButtonArray(id, (_('Defer'),
400                                      _('Approve'),
401                                      _('Reject'),
402                                      _('Discard')),
403                                 values=(mm_cfg.DEFER,
404                                         mm_cfg.SUBSCRIBE,
405                                         mm_cfg.REJECT,
406                                         mm_cfg.DISCARD),
407                                 checked=0).Format()
408        if addr not in mlist.ban_list:
409            radio += ('<br>' + '<label>' +
410                     CheckBox('ban-%d' % id, 1).Format() +
411                     '&nbsp;' + _('Permanently ban from this list') +
412                     '</label>')
413        # While the address may be a unicode, it must be ascii
414        paddr = addr.encode('us-ascii', 'replace')
415        table.AddRow(['%s<br><em>%s</em><br>%s' % (paddr,
416                                                   Utils.websafe(fullname),
417                                                   displaytime),
418                      radio,
419                      TextBox('comment-%d' % id, size=40)
420                      ])
421        num += 1
422    if num > 0:
423        form.AddItem(table)
424    return num
425
426
427
428def show_pending_unsubs(mlist, form):
429    # Add the pending unsubscription request section
430    lang = mlist.preferred_language
431    pendingunsubs = mlist.GetUnsubscriptionIds()
432    if not pendingunsubs:
433        return 0
434    table = Table(border=2)
435    table.AddRow([Center(Bold(_('User address/name'))),
436                  Center(Bold(_('Your decision'))),
437                  Center(Bold(_('Reason for refusal')))
438                  ])
439    # Alphabetical order by email address
440    byaddrs = {}
441    for id in pendingunsubs:
442        addr = mlist.GetRecord(id)
443        byaddrs.setdefault(addr, []).append(id)
444    addrs = byaddrs.items()
445    addrs.sort()
446    num = 0
447    for addr, ids in addrs:
448        # Eliminate duplicates
449        # Here the order doesn't matter as the data is just the address.
450        for id in ids[1:]:
451            mlist.HandleRequest(id, mm_cfg.DISCARD)
452        id = ids[0]
453        addr = mlist.GetRecord(id)
454        try:
455            fullname = Utils.uncanonstr(mlist.getMemberName(addr), lang)
456        except Errors.NotAMemberError:
457            # They must have been unsubscribed elsewhere, so we can just
458            # discard this record.
459            mlist.HandleRequest(id, mm_cfg.DISCARD)
460            continue
461        num += 1
462        table.AddRow(['%s<br><em>%s</em>' % (addr, Utils.websafe(fullname)),
463                      RadioButtonArray(id, (_('Defer'),
464                                            _('Approve'),
465                                            _('Reject'),
466                                            _('Discard')),
467                                       values=(mm_cfg.DEFER,
468                                               mm_cfg.UNSUBSCRIBE,
469                                               mm_cfg.REJECT,
470                                               mm_cfg.DISCARD),
471                                       checked=0),
472                      TextBox('comment-%d' % id, size=45)
473                      ])
474    if num > 0:
475        form.AddItem('<hr>')
476        form.AddItem(Center(Header(2, _('Unsubscription Requests'))))
477        form.AddItem(table)
478    return num
479
480
481
482def show_helds_overview(mlist, form, ssort=SSENDER):
483    # Sort the held messages.
484    byskey = helds_by_skey(mlist, ssort)
485    if not byskey:
486        return 0
487    form.AddItem('<hr>')
488    form.AddItem(Center(Header(2, _('Held Messages'))))
489    # Add the sort sequence choices if wanted
490    if mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS:
491        form.AddItem(Center(_('Show this list grouped/sorted by')))
492        form.AddItem(Center(hacky_radio_buttons(
493                'summary_sort',
494                (_('sender/sender'), _('sender/time'), _('ungrouped/time')),
495                (SSENDER, SSENDERTIME, STIME),
496                (ssort == SSENDER, ssort == SSENDERTIME, ssort == STIME))))
497    # Add the by-sender overview tables
498    admindburl = mlist.GetScriptURL('admindb', absolute=1)
499    table = Table(border=0)
500    form.AddItem(table)
501    skeys = byskey.keys()
502    skeys.sort()
503    for skey in skeys:
504        sender = skey[1]
505        qsender = quote_plus(sender)
506        esender = Utils.websafe(sender)
507        senderurl = admindburl + '?sender=' + qsender
508        # The encompassing sender table
509        stable = Table(border=1)
510        stable.AddRow([Center(Bold(_('From:')).Format() + esender)])
511        stable.AddCellInfo(stable.GetCurrentRowIndex(), 0, colspan=2)
512        left = Table(border=0)
513        left.AddRow([_('Action to take on all these held messages:')])
514        left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
515        btns = hacky_radio_buttons(
516            'senderaction-' + qsender,
517            (_('Defer'), _('Accept'), _('Reject'), _('Discard')),
518            (mm_cfg.DEFER, mm_cfg.APPROVE, mm_cfg.REJECT, mm_cfg.DISCARD),
519            (1, 0, 0, 0))
520        left.AddRow([btns])
521        left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
522        left.AddRow([
523            '<label>' +
524            CheckBox('senderpreserve-' + qsender, 1).Format() +
525            '&nbsp;' +
526            _('Preserve messages for the site administrator') +
527            '</label>'
528            ])
529        left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
530        left.AddRow([
531            '<label>' +
532            CheckBox('senderforward-' + qsender, 1).Format() +
533            '&nbsp;' +
534            _('Forward messages (individually) to:') +
535            '</label>'
536            ])
537        left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
538        left.AddRow([
539            TextBox('senderforwardto-' + qsender,
540                    value=mlist.GetOwnerEmail())
541            ])
542        left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
543        # If the sender is a member and the message is being held due to a
544        # moderation bit, give the admin a chance to clear the member's mod
545        # bit.  If this sender is not a member and is not already on one of
546        # the sender filters, then give the admin a chance to add this sender
547        # to one of the filters.
548        if mlist.isMember(sender):
549            if mlist.getMemberOption(sender, mm_cfg.Moderate):
550                left.AddRow([
551                    '<label>' +
552                    CheckBox('senderclearmodp-' + qsender, 1).Format() +
553                    '&nbsp;' +
554                    _("Clear this member's <em>moderate</em> flag") +
555                    '</label>'
556                    ])
557            else:
558                left.AddRow(
559                    [_('<em>The sender is now a member of this list</em>')])
560            left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
561        elif sender not in (mlist.accept_these_nonmembers +
562                            mlist.hold_these_nonmembers +
563                            mlist.reject_these_nonmembers +
564                            mlist.discard_these_nonmembers):
565            left.AddRow([
566                '<label>' +
567                CheckBox('senderfilterp-' + qsender, 1).Format() +
568                '&nbsp;' +
569                _('Add <b>%(esender)s</b> to one of these sender filters:') +
570                '</label>'
571                ])
572            left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
573            btns = hacky_radio_buttons(
574                'senderfilter-' + qsender,
575                (_('Accepts'), _('Holds'), _('Rejects'), _('Discards')),
576                (mm_cfg.ACCEPT, mm_cfg.HOLD, mm_cfg.REJECT, mm_cfg.DISCARD),
577                (0, 0, 0, 1))
578            left.AddRow([btns])
579            left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
580            if sender not in mlist.ban_list:
581                left.AddRow([
582                    '<label>' +
583                    CheckBox('senderbanp-' + qsender, 1).Format() +
584                    '&nbsp;' +
585                    _("""Ban <b>%(esender)s</b> from ever subscribing to this
586                    mailing list""") + '</label>'])
587                left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
588        right = Table(border=0)
589        right.AddRow([
590            _("""Click on the message number to view the individual
591            message, or you can """) +
592            Link(senderurl, _('view all messages from %(esender)s')).Format()
593            ])
594        right.AddCellInfo(right.GetCurrentRowIndex(), 0, colspan=2)
595        right.AddRow(['&nbsp;', '&nbsp;'])
596        counter = 1
597        for ptime, id in byskey[skey]:
598            info = mlist.GetRecord(id)
599            ptime, sender, subject, reason, filename, msgdata = info
600            # BAW: This is really the size of the message pickle, which should
601            # be close, but won't be exact.  Sigh, good enough.
602            try:
603                size = os.path.getsize(os.path.join(mm_cfg.DATA_DIR, filename))
604            except OSError, e:
605                if e.errno <> errno.ENOENT: raise
606                # This message must have gotten lost, i.e. it's already been
607                # handled by the time we got here.
608                mlist.HandleRequest(id, mm_cfg.DISCARD)
609                continue
610            dispsubj = Utils.oneline(
611                subject, Utils.GetCharSet(mlist.preferred_language))
612            t = Table(border=0)
613            t.AddRow([Link(admindburl + '?msgid=%d' % id, '[%d]' % counter),
614                      Bold(_('Subject:')),
615                      Utils.websafe(dispsubj)
616                      ])
617            t.AddRow(['&nbsp;', Bold(_('Size:')), str(size) + _(' bytes')])
618            if reason:
619                reason = _(reason)
620            else:
621                reason = _('not available')
622            t.AddRow(['&nbsp;', Bold(_('Reason:')), reason])
623            # Include the date we received the message, if available
624            when = msgdata.get('received_time')
625            if when:
626                t.AddRow(['&nbsp;', Bold(_('Received:')),
627                          time.ctime(when)])
628            t.AddRow([InputObj(qsender, 'hidden', str(id), False).Format()])
629            counter += 1
630            right.AddRow([t])
631        stable.AddRow([left, right])
632        table.AddRow([stable])
633    return 1
634
635
636
637def show_sender_requests(mlist, form, sender):
638    byskey = helds_by_skey(mlist, SSENDER)
639    if not byskey:
640        return
641    sender_ids = byskey.get((0, sender))
642    if sender_ids is None:
643        # BAW: should we print an error message?
644        return
645    sender_ids = [x[1] for x in sender_ids]
646    total = len(sender_ids)
647    count = 1
648    for id in sender_ids:
649        info = mlist.GetRecord(id)
650        show_post_requests(mlist, id, info, total, count, form)
651        count += 1
652
653
654
655def show_message_requests(mlist, form, id):
656    try:
657        id = int(id)
658        info = mlist.GetRecord(id)
659    except (ValueError, KeyError):
660        # BAW: print an error message?
661        return
662    show_post_requests(mlist, id, info, 1, 1, form)
663
664
665
666def show_detailed_requests(mlist, form):
667    all = mlist.GetHeldMessageIds()
668    total = len(all)
669    count = 1
670    for id in mlist.GetHeldMessageIds():
671        info = mlist.GetRecord(id)
672        show_post_requests(mlist, id, info, total, count, form)
673        count += 1
674
675
676
677def show_post_requests(mlist, id, info, total, count, form):
678    # Mailman.ListAdmin.__handlepost no longer tests for pre 2.0beta3
679    ptime, sender, subject, reason, filename, msgdata = info
680    form.AddItem('<hr>')
681    # Header shown on each held posting (including count of total)
682    msg = _('Posting Held for Approval')
683    if total <> 1:
684        msg += _(' (%(count)d of %(total)d)')
685    form.AddItem(Center(Header(2, msg)))
686    # We need to get the headers and part of the textual body of the message
687    # being held.  The best way to do this is to use the email Parser to get
688    # an actual object, which will be easier to deal with.  We probably could
689    # just do raw reads on the file.
690    try:
691        msg = readMessage(os.path.join(mm_cfg.DATA_DIR, filename))
692    except IOError, e:
693        if e.errno <> errno.ENOENT:
694            raise
695        form.AddItem(_('<em>Message with id #%(id)d was lost.'))
696        form.AddItem('<p>')
697        # BAW: kludge to remove id from requests.db.
698        try:
699            mlist.HandleRequest(id, mm_cfg.DISCARD)
700        except Errors.LostHeldMessage:
701            pass
702        return
703    except email.Errors.MessageParseError:
704        form.AddItem(_('<em>Message with id #%(id)d is corrupted.'))
705        # BAW: Should we really delete this, or shuttle it off for site admin
706        # to look more closely at?
707        form.AddItem('<p>')
708        # BAW: kludge to remove id from requests.db.
709        try:
710            mlist.HandleRequest(id, mm_cfg.DISCARD)
711        except Errors.LostHeldMessage:
712            pass
713        return
714    # Get the header text and the message body excerpt
715    lines = []
716    chars = 0
717    # A negative value means, include the entire message regardless of size
718    limit = mm_cfg.ADMINDB_PAGE_TEXT_LIMIT
719    for line in email.Iterators.body_line_iterator(msg, decode=True):
720        lines.append(line)
721        chars += len(line)
722        if chars >= limit > 0:
723            break
724    # We may have gone over the limit on the last line, but keep the full line
725    # anyway to avoid losing part of a multibyte character.
726    body = EMPTYSTRING.join(lines)
727    # Get message charset and try encode in list charset
728    # We get it from the first text part.
729    # We need to replace invalid characters here or we can throw an uncaught
730    # exception in doc.Format().
731    for part in msg.walk():
732        if part.get_content_maintype() == 'text':
733            # Watchout for charset= with no value.
734            mcset = part.get_content_charset() or 'us-ascii'
735            break
736    else:
737        mcset = 'us-ascii'
738    lcset = Utils.GetCharSet(mlist.preferred_language)
739    if mcset <> lcset:
740        try:
741            body = unicode(body, mcset, 'replace').encode(lcset, 'replace')
742        except (LookupError, UnicodeError, ValueError):
743            pass
744    hdrtxt = NL.join(['%s: %s' % (k, v) for k, v in msg.items()])
745    hdrtxt = Utils.websafe(hdrtxt)
746    # Okay, we've reconstituted the message just fine.  Now for the fun part!
747    t = Table(cellspacing=0, cellpadding=0, width='100%')
748    t.AddRow([Bold(_('From:')), sender])
749    row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
750    t.AddCellInfo(row, col-1, align='right')
751    t.AddRow([Bold(_('Subject:')),
752              Utils.websafe(Utils.oneline(subject, lcset))])
753    t.AddCellInfo(row+1, col-1, align='right')
754    t.AddRow([Bold(_('Reason:')), _(reason)])
755    t.AddCellInfo(row+2, col-1, align='right')
756    when = msgdata.get('received_time')
757    if when:
758        t.AddRow([Bold(_('Received:')), time.ctime(when)])
759        t.AddCellInfo(row+3, col-1, align='right')
760    buttons = hacky_radio_buttons(id,
761                (_('Defer'), _('Approve'), _('Reject'), _('Discard')),
762                (mm_cfg.DEFER, mm_cfg.APPROVE, mm_cfg.REJECT, mm_cfg.DISCARD),
763                (1, 0, 0, 0),
764                spacing=5)
765    t.AddRow([Bold(_('Action:')), buttons])
766    t.AddCellInfo(t.GetCurrentRowIndex(), col-1, align='right')
767    t.AddRow(['&nbsp;',
768              '<label>' +
769              CheckBox('preserve-%d' % id, 'on', 0).Format() +
770              '&nbsp;' + _('Preserve message for site administrator') +
771              '</label>'
772              ])
773    t.AddRow(['&nbsp;',
774              '<label>' +
775              CheckBox('forward-%d' % id, 'on', 0).Format() +
776              '&nbsp;' + _('Additionally, forward this message to: ') +
777              '</label>' +
778              TextBox('forward-addr-%d' % id, size=47,
779                      value=mlist.GetOwnerEmail()).Format()
780              ])
781    notice = msgdata.get('rejection_notice', _('[No explanation given]'))
782    t.AddRow([
783        Bold(_('If you reject this post,<br>please explain (optional):')),
784        TextArea('comment-%d' % id, rows=4, cols=EXCERPT_WIDTH,
785                 text = Utils.wrap(_(notice), column=80))
786        ])
787    row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
788    t.AddCellInfo(row, col-1, align='right')
789    t.AddRow([Bold(_('Message Headers:')),
790              TextArea('headers-%d' % id, hdrtxt,
791                       rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)])
792    row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
793    t.AddCellInfo(row, col-1, align='right')
794    t.AddRow([Bold(_('Message Excerpt:')),
795              TextArea('fulltext-%d' % id, Utils.websafe(body),
796                       rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)])
797    t.AddCellInfo(row+1, col-1, align='right')
798    form.AddItem(t)
799    form.AddItem('<p>')
800
801
802
803def process_form(mlist, doc, cgidata):
804    global ssort
805    senderactions = {}
806    badaddrs = []
807    # Sender-centric actions
808    for k in cgidata.keys():
809        for prefix in ('senderaction-', 'senderpreserve-', 'senderforward-',
810                       'senderforwardto-', 'senderfilterp-', 'senderfilter-',
811                       'senderclearmodp-', 'senderbanp-'):
812            if k.startswith(prefix):
813                action = k[:len(prefix)-1]
814                qsender = k[len(prefix):]
815                sender = unquote_plus(qsender)
816                value = cgidata.getfirst(k)
817                senderactions.setdefault(sender, {})[action] = value
818                for id in cgidata.getlist(qsender):
819                    senderactions[sender].setdefault('message_ids',
820                                                     []).append(int(id))
821    # discard-all-defers
822    try:
823        discardalldefersp = cgidata.getfirst('discardalldefersp', 0)
824    except ValueError:
825        discardalldefersp = 0
826    # Get the summary sequence
827    ssort = int(cgidata.getfirst('summary_sort', SSENDER))
828    for sender in senderactions.keys():
829        actions = senderactions[sender]
830        # Handle what to do about all this sender's held messages
831        try:
832            action = int(actions.get('senderaction', mm_cfg.DEFER))
833        except ValueError:
834            action = mm_cfg.DEFER
835        if action == mm_cfg.DEFER and discardalldefersp:
836            action = mm_cfg.DISCARD
837        if action in (mm_cfg.DEFER, mm_cfg.APPROVE,
838                      mm_cfg.REJECT, mm_cfg.DISCARD):
839            preserve = actions.get('senderpreserve', 0)
840            forward = actions.get('senderforward', 0)
841            forwardaddr = actions.get('senderforwardto', '')
842            byskey = helds_by_skey(mlist, SSENDER)
843            for ptime, id in byskey.get((0, sender), []):
844                if id not in senderactions[sender]['message_ids']:
845                    # It arrived after the page was displayed. Skip it.
846                    continue
847                try:
848                    msgdata = mlist.GetRecord(id)[5]
849                    comment = msgdata.get('rejection_notice',
850                                      _('[No explanation given]'))
851                    mlist.HandleRequest(id, action, comment, preserve,
852                                        forward, forwardaddr)
853                except (KeyError, Errors.LostHeldMessage):
854                    # That's okay, it just means someone else has already
855                    # updated the database while we were staring at the page,
856                    # so just ignore it
857                    continue
858        # Now see if this sender should be added to one of the nonmember
859        # sender filters.
860        if actions.get('senderfilterp', 0):
861            # Check for an invalid sender address.
862            try:
863                Utils.ValidateEmail(sender)
864            except Errors.EmailAddressError:
865                # Don't check for dups.  Report it once for each checked box.
866                badaddrs.append(sender)
867            else:
868                try:
869                    which = int(actions.get('senderfilter'))
870                except ValueError:
871                    # Bogus form
872                    which = 'ignore'
873                if which == mm_cfg.ACCEPT:
874                    mlist.accept_these_nonmembers.append(sender)
875                elif which == mm_cfg.HOLD:
876                    mlist.hold_these_nonmembers.append(sender)
877                elif which == mm_cfg.REJECT:
878                    mlist.reject_these_nonmembers.append(sender)
879                elif which == mm_cfg.DISCARD:
880                    mlist.discard_these_nonmembers.append(sender)
881                # Otherwise, it's a bogus form, so ignore it
882        # And now see if we're to clear the member's moderation flag.
883        if actions.get('senderclearmodp', 0):
884            try:
885                mlist.setMemberOption(sender, mm_cfg.Moderate, 0)
886            except Errors.NotAMemberError:
887                # This person's not a member any more.  Oh well.
888                pass
889        # And should this address be banned?
890        if actions.get('senderbanp', 0):
891            # Check for an invalid sender address.
892            try:
893                Utils.ValidateEmail(sender)
894            except Errors.EmailAddressError:
895                # Don't check for dups.  Report it once for each checked box.
896                badaddrs.append(sender)
897            else:
898                if sender not in mlist.ban_list:
899                    mlist.ban_list.append(sender)
900    # Now, do message specific actions
901    banaddrs = []
902    erroraddrs = []
903    for k in cgidata.keys():
904        formv = cgidata[k]
905        if type(formv) == ListType:
906            continue
907        try:
908            v = int(formv.value)
909            request_id = int(k)
910        except ValueError:
911            continue
912        if v not in (mm_cfg.DEFER, mm_cfg.APPROVE, mm_cfg.REJECT,
913                     mm_cfg.DISCARD, mm_cfg.SUBSCRIBE, mm_cfg.UNSUBSCRIBE,
914                     mm_cfg.ACCEPT, mm_cfg.HOLD):
915            continue
916        # Get the action comment and reasons if present.
917        commentkey = 'comment-%d' % request_id
918        preservekey = 'preserve-%d' % request_id
919        forwardkey = 'forward-%d' % request_id
920        forwardaddrkey = 'forward-addr-%d' % request_id
921        bankey = 'ban-%d' % request_id
922        # Defaults
923        try:
924            if mlist.GetRecordType(request_id) == HELDMSG:
925                msgdata = mlist.GetRecord(request_id)[5]
926                comment = msgdata.get('rejection_notice',
927                                      _('[No explanation given]'))
928            else:
929                comment = _('[No explanation given]')
930        except KeyError:
931            # Someone else must have handled this one after we got the page.
932            continue
933        preserve = 0
934        forward = 0
935        forwardaddr = ''
936        if cgidata.has_key(commentkey):
937            comment = cgidata[commentkey].value
938        if cgidata.has_key(preservekey):
939            preserve = cgidata[preservekey].value
940        if cgidata.has_key(forwardkey):
941            forward = cgidata[forwardkey].value
942        if cgidata.has_key(forwardaddrkey):
943            forwardaddr = cgidata[forwardaddrkey].value
944        # Should we ban this address?  Do this check before handling the
945        # request id because that will evict the record.
946        if cgidata.getfirst(bankey):
947            sender = mlist.GetRecord(request_id)[1]
948            if sender not in mlist.ban_list:
949                # We don't need to validate the sender.  An invalid address
950                # can't get here.
951                mlist.ban_list.append(sender)
952        # Handle the request id
953        try:
954            mlist.HandleRequest(request_id, v, comment,
955                                preserve, forward, forwardaddr)
956        except (KeyError, Errors.LostHeldMessage):
957            # That's okay, it just means someone else has already updated the
958            # database while we were staring at the page, so just ignore it
959            continue
960        except Errors.MMAlreadyAMember, v:
961            erroraddrs.append(v)
962        except Errors.MembershipIsBanned, pattern:
963            sender = mlist.GetRecord(request_id)[1]
964            banaddrs.append((sender, pattern))
965    # save the list and print the results
966    doc.AddItem(Header(2, _('Database Updated...')))
967    if erroraddrs:
968        for addr in erroraddrs:
969            addr = Utils.websafe(addr)
970            doc.AddItem(`addr` + _(' is already a member') + '<br>')
971    if banaddrs:
972        for addr, patt in banaddrs:
973            addr = Utils.websafe(addr)
974            doc.AddItem(_('%(addr)s is banned (matched: %(patt)s)') + '<br>')
975    if badaddrs:
976        for addr in badaddrs:
977            addr = Utils.websafe(addr)
978            doc.AddItem(`addr` + ': ' + _('Bad/Invalid email address') +
979                        '<br>')
980