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

admindb.py
Go to the documentation of this file.
1# Copyright (C) 1998-2018 by the Free Software Foundation, Inc.
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16# USA.
17
18"""Produce and 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:
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)
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)
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)
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)
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)
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)
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>')
def csrf_check(mlist, token, cgi_user=None)
Definition: CSRFcheck.py:58
def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3)
Definition: admindb.py:91
def show_pending_unsubs(mlist, form)
Definition: admindb.py:428
def process_form(mlist, doc, cgidata)
Definition: admindb.py:803
def show_post_requests(mlist, id, info, total, count, form)
Definition: admindb.py:677
def show_message_requests(mlist, form, id)
Definition: admindb.py:655
def handle_no_list(msg='')
Definition: admindb.py:352
def show_sender_requests(mlist, form, sender)
Definition: admindb.py:637
def show_helds_overview(mlist, form, ssort=SSENDER)
Definition: admindb.py:482
def show_detailed_requests(mlist, form)
Definition: admindb.py:666
def show_pending_subs(mlist, form)
Definition: admindb.py:370
def helds_by_skey(mlist, ssort=SSENDER)
Definition: admindb.py:67
def readMessage(path)
Definition: ListAdmin.py:614