"""Produce and process the pending-approval items for a list."""
import sys
import os
import cgi
import errno
import signal
import email
import time
from types import ListType
from urllib import quote_plus, unquote_plus
from Mailman import mm_cfg
from Mailman import Utils
from Mailman import MailList
from Mailman import Errors
from Mailman import Message
from Mailman import i18n
from Mailman.Handlers.Moderate import ModeratedMemberPost
from Mailman.ListAdmin import HELDMSG
from Mailman.ListAdmin import readMessage
from Mailman.Cgi import Auth
from Mailman.htmlformat import *
from Mailman.Logging.Syslog import syslog
EMPTYSTRING = ''
NL = '\n'
_ = i18n._
i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
EXCERPT_HEIGHT = 10
EXCERPT_WIDTH = 76
def helds_by_sender(mlist):
heldmsgs = mlist.GetHeldMessageIds()
bysender = {}
for id in heldmsgs:
sender = mlist.GetRecord(id)[1]
bysender.setdefault(sender, []).append(id)
return bysender
def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3):
space = ' ' * spacing
btns = Table(cellspacing='5', cellpadding='0')
btns.AddRow([space + text + space for text in labels])
btns.AddRow([Center(RadioButton(btnname, value, default))
for value, default in zip(values, defaults)])
return btns
def main():
parts = Utils.GetPathPieces()
if not parts:
handle_no_list()
return
listname = parts[0].lower()
try:
mlist = MailList.MailList(listname, lock=0)
except Errors.MMListError, e:
safelistname = Utils.websafe(listname)
handle_no_list(_('No such list <em>%(safelistname)s</em>'))
syslog('error', 'No such list "%s": %s\n', listname, e)
return
i18n.set_language(mlist.preferred_language)
cgidata = cgi.FieldStorage(keep_blank_values=1)
if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
mm_cfg.AuthListModerator,
mm_cfg.AuthSiteAdmin),
cgidata.getvalue('adminpw', '')):
if cgidata.has_key('adminpw'):
msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
else:
msg = ''
Auth.loginpage(mlist, 'admindb', msg=msg)
return
doc = Document()
doc.set_language(mlist.preferred_language)
sender = None
msgid = None
details = None
envar = os.environ.get('QUERY_STRING')
if envar:
qs = cgi.parse_qs(envar).get('sender')
if qs and type(qs) == ListType:
sender = qs[0]
qs = cgi.parse_qs(envar).get('msgid')
if qs and type(qs) == ListType:
msgid = qs[0]
qs = cgi.parse_qs(envar).get('details')
if qs and type(qs) == ListType:
details = qs[0]
def sigterm_handler(signum, frame, mlist=mlist):
mlist.Unlock()
sys.exit(0)
mlist.Lock()
try:
signal.signal(signal.SIGTERM, sigterm_handler)
realname = mlist.real_name
if not cgidata.keys() or cgidata.has_key('admlogin'):
doc.SetTitle(_('%(realname)s Administrative Database'))
elif not details:
doc.SetTitle(_('%(realname)s Administrative Database Results'))
process_form(mlist, doc, cgidata)
if not mlist.NumRequestsPending():
title = _('%(realname)s Administrative Database')
doc.SetTitle(title)
doc.AddItem(Header(2, title))
doc.AddItem(_('There are no pending requests.'))
doc.AddItem(' ')
doc.AddItem(Link(mlist.GetScriptURL('admindb', absolute=1),
_('Click here to reload this page.')))
doc.AddItem(mlist.GetMailmanFooter())
print doc.Format()
mlist.Save()
return
admindburl = mlist.GetScriptURL('admindb', absolute=1)
form = Form(admindburl)
if details == 'instructions':
doc.AddItem(Header(
2, _('Detailed instructions for the administrative database')))
else:
doc.AddItem(Header(
2,
_('Administrative requests for mailing list:')
+ ' <em>%s</em>' % mlist.real_name))
if details <> 'instructions':
form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
nomessages = not mlist.GetHeldMessageIds()
if not (details or sender or msgid or nomessages):
form.AddItem(Center(
CheckBox('discardalldefersp', 0).Format() +
' ' +
_('Discard all messages marked <em>Defer</em>')
))
adminurl = mlist.GetScriptURL('admin', absolute=1)
d = {'listname' : mlist.real_name,
'detailsurl': admindburl + '?details=instructions',
'summaryurl': admindburl,
'viewallurl': admindburl + '?details=all',
'adminurl' : adminurl,
'filterurl' : adminurl + '/privacy/sender',
}
addform = 1
if sender:
esender = Utils.websafe(sender)
d['description'] = _("all of %(esender)s's held messages.")
doc.AddItem(Utils.maketext('admindbpreamble.html', d,
raw=1, mlist=mlist))
show_sender_requests(mlist, form, sender)
elif msgid:
d['description'] = _('a single held message.')
doc.AddItem(Utils.maketext('admindbpreamble.html', d,
raw=1, mlist=mlist))
show_message_requests(mlist, form, msgid)
elif details == 'all':
d['description'] = _('all held messages.')
doc.AddItem(Utils.maketext('admindbpreamble.html', d,
raw=1, mlist=mlist))
show_detailed_requests(mlist, form)
elif details == 'instructions':
doc.AddItem(Utils.maketext('admindbdetails.html', d,
raw=1, mlist=mlist))
addform = 0
else:
doc.AddItem(Utils.maketext('admindbsummary.html', d,
raw=1, mlist=mlist))
num = show_pending_subs(mlist, form)
num += show_pending_unsubs(mlist, form)
num += show_helds_overview(mlist, form)
addform = num > 0
if addform:
doc.AddItem(form)
form.AddItem('<hr>')
if not (details or sender or msgid or nomessages):
form.AddItem(Center(
CheckBox('discardalldefersp', 0).Format() +
' ' +
_('Discard all messages marked <em>Defer</em>')
))
form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
doc.AddItem(mlist.GetMailmanFooter())
print doc.Format()
mlist.Save()
finally:
mlist.Unlock()
def handle_no_list(msg=''):
doc = Document()
doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
header = _('Mailman Administrative Database Error')
doc.SetTitle(header)
doc.AddItem(Header(2, header))
doc.AddItem(msg)
url = Utils.ScriptURL('admin', absolute=1)
link = Link(url, _('list of available mailing lists.')).Format()
doc.AddItem(_('You must specify a list name. Here is the %(link)s'))
doc.AddItem('<hr>')
doc.AddItem(MailmanLogo())
print doc.Format()
def show_pending_subs(mlist, form):
pendingsubs = mlist.GetSubscriptionIds()
if not pendingsubs:
return 0
form.AddItem('<hr>')
form.AddItem(Center(Header(2, _('Subscription Requests'))))
table = Table(border=2)
table.AddRow([Center(Bold(_('Address/name'))),
Center(Bold(_('Your decision'))),
Center(Bold(_('Reason for refusal')))
])
byaddrs = {}
for id in pendingsubs:
addr = mlist.GetRecord(id)[1]
byaddrs.setdefault(addr, []).append(id)
addrs = byaddrs.keys()
addrs.sort()
num = 0
for addr, ids in byaddrs.items():
for id in ids[1:]:
mlist.HandleRequest(id, mm_cfg.DISCARD)
id = ids[0]
time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
fullname = Utils.uncanonstr(fullname, mlist.preferred_language)
radio = RadioButtonArray(id, (_('Defer'),
_('Approve'),
_('Reject'),
_('Discard')),
values=(mm_cfg.DEFER,
mm_cfg.SUBSCRIBE,
mm_cfg.REJECT,
mm_cfg.DISCARD),
checked=0).Format()
if addr not in mlist.ban_list:
radio += '<br>' + CheckBox('ban-%d' % id, 1).Format() + \
' ' + _('Permanently ban from this list')
paddr = addr.encode('us-ascii', 'replace')
table.AddRow(['%s<br><em>%s</em>' % (paddr, Utils.websafe(fullname)),
radio,
TextBox('comment-%d' % id, size=40)
])
num += 1
if num > 0:
form.AddItem(table)
return num
def show_pending_unsubs(mlist, form):
lang = mlist.preferred_language
pendingunsubs = mlist.GetUnsubscriptionIds()
if not pendingunsubs:
return 0
table = Table(border=2)
table.AddRow([Center(Bold(_('User address/name'))),
Center(Bold(_('Your decision'))),
Center(Bold(_('Reason for refusal')))
])
byaddrs = {}
for id in pendingunsubs:
addr = mlist.GetRecord(id)
byaddrs.setdefault(addr, []).append(id)
addrs = byaddrs.keys()
addrs.sort()
num = 0
for addr, ids in byaddrs.items():
for id in ids[1:]:
mlist.HandleRequest(id, mm_cfg.DISCARD)
id = ids[0]
addr = mlist.GetRecord(id)
try:
fullname = Utils.uncanonstr(mlist.getMemberName(addr), lang)
except Errors.NotAMemberError:
mlist.HandleRequest(id, mm_cfg.DISCARD)
continue
num += 1
table.AddRow(['%s<br><em>%s</em>' % (addr, Utils.websafe(fullname)),
RadioButtonArray(id, (_('Defer'),
_('Approve'),
_('Reject'),
_('Discard')),
values=(mm_cfg.DEFER,
mm_cfg.UNSUBSCRIBE,
mm_cfg.REJECT,
mm_cfg.DISCARD),
checked=0),
TextBox('comment-%d' % id, size=45)
])
if num > 0:
form.AddItem('<hr>')
form.AddItem(Center(Header(2, _('Unsubscription Requests'))))
form.AddItem(table)
return num
def show_helds_overview(mlist, form):
bysender = helds_by_sender(mlist)
if not bysender:
return 0
form.AddItem('<hr>')
form.AddItem(Center(Header(2, _('Held Messages'))))
admindburl = mlist.GetScriptURL('admindb', absolute=1)
table = Table(border=0)
form.AddItem(table)
senders = bysender.keys()
senders.sort()
for sender in senders:
qsender = quote_plus(sender)
esender = Utils.websafe(sender)
senderurl = admindburl + '?sender=' + qsender
stable = Table(border=1)
stable.AddRow([Center(Bold(_('From:')).Format() + esender)])
stable.AddCellInfo(stable.GetCurrentRowIndex(), 0, colspan=2)
left = Table(border=0)
left.AddRow([_('Action to take on all these held messages:')])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
btns = hacky_radio_buttons(
'senderaction-' + qsender,
(_('Defer'), _('Accept'), _('Reject'), _('Discard')),
(mm_cfg.DEFER, mm_cfg.APPROVE, mm_cfg.REJECT, mm_cfg.DISCARD),
(1, 0, 0, 0))
left.AddRow([btns])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
left.AddRow([
CheckBox('senderpreserve-' + qsender, 1).Format() +
' ' +
_('Preserve messages for the site administrator')
])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
left.AddRow([
CheckBox('senderforward-' + qsender, 1).Format() +
' ' +
_('Forward messages (individually) to:')
])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
left.AddRow([
TextBox('senderforwardto-' + qsender,
value=mlist.GetOwnerEmail())
])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
if mlist.isMember(sender):
if mlist.getMemberOption(sender, mm_cfg.Moderate):
left.AddRow([
CheckBox('senderclearmodp-' + qsender, 1).Format() +
' ' +
_("Clear this member's <em>moderate</em> flag")
])
else:
left.AddRow(
[_('<em>The sender is now a member of this list</em>')])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
elif sender not in (mlist.accept_these_nonmembers +
mlist.hold_these_nonmembers +
mlist.reject_these_nonmembers +
mlist.discard_these_nonmembers):
left.AddRow([
CheckBox('senderfilterp-' + qsender, 1).Format() +
' ' +
_('Add <b>%(esender)s</b> to one of these sender filters:')
])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
btns = hacky_radio_buttons(
'senderfilter-' + qsender,
(_('Accepts'), _('Holds'), _('Rejects'), _('Discards')),
(mm_cfg.ACCEPT, mm_cfg.HOLD, mm_cfg.REJECT, mm_cfg.DISCARD),
(0, 0, 0, 1))
left.AddRow([btns])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
if sender not in mlist.ban_list:
left.AddRow([
CheckBox('senderbanp-' + qsender, 1).Format() +
' ' +
_("""Ban <b>%(esender)s</b> from ever subscribing to this
mailing list""")])
left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
right = Table(border=0)
right.AddRow([
_("""Click on the message number to view the individual
message, or you can """) +
Link(senderurl, _('view all messages from %(esender)s')).Format()
])
right.AddCellInfo(right.GetCurrentRowIndex(), 0, colspan=2)
right.AddRow([' ', ' '])
counter = 1
for id in bysender[sender]:
info = mlist.GetRecord(id)
ptime, sender, subject, reason, filename, msgdata = info
try:
size = os.path.getsize(os.path.join(mm_cfg.DATA_DIR, filename))
except OSError, e:
if e.errno <> errno.ENOENT: raise
mlist.HandleRequest(id, mm_cfg.DISCARD)
continue
dispsubj = Utils.oneline(
subject, Utils.GetCharSet(mlist.preferred_language))
t = Table(border=0)
t.AddRow([Link(admindburl + '?msgid=%d' % id, '[%d]' % counter),
Bold(_('Subject:')),
Utils.websafe(dispsubj)
])
t.AddRow([' ', Bold(_('Size:')), str(size) + _(' bytes')])
if reason:
reason = _(reason)
else:
reason = _('not available')
t.AddRow([' ', Bold(_('Reason:')), reason])
when = msgdata.get('received_time')
if when:
t.AddRow([' ', Bold(_('Received:')),
time.ctime(when)])
counter += 1
right.AddRow([t])
stable.AddRow([left, right])
table.AddRow([stable])
return 1
def show_sender_requests(mlist, form, sender):
bysender = helds_by_sender(mlist)
if not bysender:
return
sender_ids = bysender.get(sender)
if sender_ids is None:
return
total = len(sender_ids)
count = 1
for id in sender_ids:
info = mlist.GetRecord(id)
show_post_requests(mlist, id, info, total, count, form)
count += 1
def show_message_requests(mlist, form, id):
try:
id = int(id)
info = mlist.GetRecord(id)
except (ValueError, KeyError):
return
show_post_requests(mlist, id, info, 1, 1, form)
def show_detailed_requests(mlist, form):
all = mlist.GetHeldMessageIds()
total = len(all)
count = 1
for id in mlist.GetHeldMessageIds():
info = mlist.GetRecord(id)
show_post_requests(mlist, id, info, total, count, form)
count += 1
def show_post_requests(mlist, id, info, total, count, form):
ptime, sender, subject, reason, filename, msgdata = info
form.AddItem('<hr>')
msg = _('Posting Held for Approval')
if total <> 1:
msg += _(' (%(count)d of %(total)d)')
form.AddItem(Center(Header(2, msg)))
try:
msg = readMessage(os.path.join(mm_cfg.DATA_DIR, filename))
except IOError, e:
if e.errno <> errno.ENOENT:
raise
form.AddItem(_('<em>Message with id #%(id)d was lost.'))
form.AddItem('<p>')
try:
mlist.HandleRequest(id, mm_cfg.DISCARD)
except Errors.LostHeldMessage:
pass
return
except email.Errors.MessageParseError:
form.AddItem(_('<em>Message with id #%(id)d is corrupted.'))
form.AddItem('<p>')
try:
mlist.HandleRequest(id, mm_cfg.DISCARD)
except Errors.LostHeldMessage:
pass
return
lines = []
chars = 0
limit = mm_cfg.ADMINDB_PAGE_TEXT_LIMIT
for line in email.Iterators.body_line_iterator(msg):
lines.append(line)
chars += len(line)
if chars > limit > 0:
break
if limit > 0:
body = EMPTYSTRING.join(lines)[:mm_cfg.ADMINDB_PAGE_TEXT_LIMIT]
else:
body = EMPTYSTRING.join(lines)
mcset = msg.get_param('charset', 'us-ascii').lower()
lcset = Utils.GetCharSet(mlist.preferred_language)
if mcset <> lcset:
try:
body = unicode(body, mcset).encode(lcset)
except (LookupError, UnicodeError, ValueError):
pass
hdrtxt = NL.join(['%s: %s' % (k, v) for k, v in msg.items()])
hdrtxt = Utils.websafe(hdrtxt)
t = Table(cellspacing=0, cellpadding=0, width='100%')
t.AddRow([Bold(_('From:')), sender])
row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
t.AddCellInfo(row, col-1, align='right')
t.AddRow([Bold(_('Subject:')),
Utils.websafe(Utils.oneline(subject, lcset))])
t.AddCellInfo(row+1, col-1, align='right')
t.AddRow([Bold(_('Reason:')), _(reason)])
t.AddCellInfo(row+2, col-1, align='right')
when = msgdata.get('received_time')
if when:
t.AddRow([Bold(_('Received:')), time.ctime(when)])
t.AddCellInfo(row+2, col-1, align='right')
buttons = Table(cellspacing="5", cellpadding="0")
buttons.AddRow(map(lambda x, s=' '*5: s+x+s,
(_('Defer'), _('Approve'), _('Reject'), _('Discard'))))
buttons.AddRow([Center(RadioButton(id, mm_cfg.DEFER, 1)),
Center(RadioButton(id, mm_cfg.APPROVE, 0)),
Center(RadioButton(id, mm_cfg.REJECT, 0)),
Center(RadioButton(id, mm_cfg.DISCARD, 0)),
])
t.AddRow([Bold(_('Action:')), buttons])
t.AddCellInfo(row+3, col-1, align='right')
t.AddRow([' ',
CheckBox('preserve-%d' % id, 'on', 0).Format() +
' ' + _('Preserve message for site administrator')
])
t.AddRow([' ',
CheckBox('forward-%d' % id, 'on', 0).Format() +
' ' + _('Additionally, forward this message to: ') +
TextBox('forward-addr-%d' % id, size=47,
value=mlist.GetOwnerEmail()).Format()
])
notice = msgdata.get('rejection_notice', _('[No explanation given]'))
t.AddRow([
Bold(_('If you reject this post,<br>please explain (optional):')),
TextArea('comment-%d' % id, rows=4, cols=EXCERPT_WIDTH,
text = Utils.wrap(_(notice), column=80))
])
row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
t.AddCellInfo(row, col-1, align='right')
t.AddRow([Bold(_('Message Headers:')),
TextArea('headers-%d' % id, hdrtxt,
rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)])
row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex()
t.AddCellInfo(row, col-1, align='right')
t.AddRow([Bold(_('Message Excerpt:')),
TextArea('fulltext-%d' % id, Utils.websafe(body),
rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)])
t.AddCellInfo(row+1, col-1, align='right')
form.AddItem(t)
form.AddItem('<p>')
def process_form(mlist, doc, cgidata):
senderactions = {}
for k in cgidata.keys():
for prefix in ('senderaction-', 'senderpreserve-', 'senderforward-',
'senderforwardto-', 'senderfilterp-', 'senderfilter-',
'senderclearmodp-', 'senderbanp-'):
if k.startswith(prefix):
action = k[:len(prefix)-1]
sender = unquote_plus(k[len(prefix):])
value = cgidata.getvalue(k)
senderactions.setdefault(sender, {})[action] = value
try:
discardalldefersp = cgidata.getvalue('discardalldefersp', 0)
except ValueError:
discardalldefersp = 0
for sender in senderactions.keys():
actions = senderactions[sender]
try:
action = int(actions.get('senderaction', mm_cfg.DEFER))
except ValueError:
action = mm_cfg.DEFER
if action == mm_cfg.DEFER and discardalldefersp:
action = mm_cfg.DISCARD
if action in (mm_cfg.DEFER, mm_cfg.APPROVE,
mm_cfg.REJECT, mm_cfg.DISCARD):
preserve = actions.get('senderpreserve', 0)
forward = actions.get('senderforward', 0)
forwardaddr = actions.get('senderforwardto', '')
bysender = helds_by_sender(mlist)
for id in bysender.get(sender, []):
try:
msgdata = mlist.GetRecord(id)[5]
comment = msgdata.get('rejection_notice',
_('[No explanation given]'))
mlist.HandleRequest(id, action, comment, preserve,
forward, forwardaddr)
except (KeyError, Errors.LostHeldMessage):
continue
if actions.get('senderfilterp', 0):
try:
which = int(actions.get('senderfilter'))
except ValueError:
which = 'ignore'
if which == mm_cfg.ACCEPT:
mlist.accept_these_nonmembers.append(sender)
elif which == mm_cfg.HOLD:
mlist.hold_these_nonmembers.append(sender)
elif which == mm_cfg.REJECT:
mlist.reject_these_nonmembers.append(sender)
elif which == mm_cfg.DISCARD:
mlist.discard_these_nonmembers.append(sender)
if actions.get('senderclearmodp', 0):
try:
mlist.setMemberOption(sender, mm_cfg.Moderate, 0)
except Errors.NotAMemberError:
pass
if actions.get('senderbanp', 0):
if sender not in mlist.ban_list:
mlist.ban_list.append(sender)
banaddrs = []
erroraddrs = []
for k in cgidata.keys():
formv = cgidata[k]
if type(formv) == ListType:
continue
try:
v = int(formv.value)
request_id = int(k)
except ValueError:
continue
if v not in (mm_cfg.DEFER, mm_cfg.APPROVE, mm_cfg.REJECT,
mm_cfg.DISCARD, mm_cfg.SUBSCRIBE, mm_cfg.UNSUBSCRIBE,
mm_cfg.ACCEPT, mm_cfg.HOLD):
continue
commentkey = 'comment-%d' % request_id
preservekey = 'preserve-%d' % request_id
forwardkey = 'forward-%d' % request_id
forwardaddrkey = 'forward-addr-%d' % request_id
bankey = 'ban-%d' % request_id
if mlist.GetRecordType(request_id) == HELDMSG:
msgdata = mlist.GetRecord(request_id)[5]
comment = msgdata.get('rejection_notice',
_('[No explanation given]'))
else:
comment = _('[No explanation given]')
preserve = 0
forward = 0
forwardaddr = ''
if cgidata.has_key(commentkey):
comment = cgidata[commentkey].value
if cgidata.has_key(preservekey):
preserve = cgidata[preservekey].value
if cgidata.has_key(forwardkey):
forward = cgidata[forwardkey].value
if cgidata.has_key(forwardaddrkey):
forwardaddr = cgidata[forwardaddrkey].value
if cgidata.getvalue(bankey):
sender = mlist.GetRecord(request_id)[1]
if sender not in mlist.ban_list:
mlist.ban_list.append(sender)
try:
mlist.HandleRequest(request_id, v, comment,
preserve, forward, forwardaddr)
except (KeyError, Errors.LostHeldMessage):
continue
except Errors.MMAlreadyAMember, v:
erroraddrs.append(v)
except Errors.MembershipIsBanned, pattern:
sender = mlist.GetRecord(request_id)[1]
banaddrs.append((sender, pattern))
doc.AddItem(Header(2, _('Database Updated...')))
if erroraddrs:
for addr in erroraddrs:
doc.AddItem(`addr` + _(' is already a member') + '<br>')
if banaddrs:
for addr, patt in banaddrs:
doc.AddItem(_('%(addr)s is banned (matched: %(patt)s)') + '<br>')