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 ' ' + 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 ' ' + 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 ' ' + _('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 ' ' + 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 ' ' + 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 ' ' + 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 ' ' + 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 ' ' + 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([' ', ' ']) 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([' ', Bold(_('Size:')), str(size) + _(' bytes')]) 618 if reason: 619 reason = _(reason) 620 else: 621 reason = _('not available') 622 t.AddRow([' ', 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([' ', 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([' ', 768 '<label>' + 769 CheckBox('preserve-%d' % id, 'on', 0).Format() + 770 ' ' + _('Preserve message for site administrator') + 771 '</label>' 772 ]) 773 t.AddRow([' ', 774 '<label>' + 775 CheckBox('forward-%d' % id, 'on', 0).Format() + 776 ' ' + _('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