#!/usr/local/bin/python
"""Process and produce the list-administration options forms.
To run stand-alone for debugging, set env var PATH_INFO to name of list
and, optionally, options category."""
__version__ = "$Revision: 528 $"
import sys
sys.path.append('/home/mailman/mailman/modules')
import os, cgi, string, crypt, types
import mm_utils, maillist, mm_cfg, mm_err
from htmlformat import *
try:
sys.stderr = mm_utils.StampedLogger("error", label = 'admin',
manual_reprime=1, nofail=0)
except IOError:
pass # Oh well - SOL on redirect, errors show thru.
CATEGORIES = [('general', "General Options"),
('members', "Membership Management"),
('privacy', "Privacy Options"),
('nondigest', "Regular-member (non-digest) Options"),
('digest', "Digest-member Options"),
('bounce', "Bounce Options"),
('archive', "Archival Options")]
def main():
"""Process and produce list options form.
CGI input indicates that we're returning from submission of some new
settings, which is processed before producing the new version."""
global list_name, list_info
doc = Document()
try:
path = os.environ['PATH_INFO']
except KeyError:
path = ""
list_info = mm_utils.GetPathPieces(path)
if len(list_info) == 0:
FormatAdminOverview()
return
list_name = string.lower(list_info[0])
lst = maillist.MailList(list_name)
try:
if not (lst and lst._ready):
FormatAdminOverview(error="List %s not found."
% list_name)
return
if len(list_info) == 1:
category = 'general'
category_suffix = ''
else:
category = list_info[1]
category_suffix = category
if category not in map(lambda x: x[0], CATEGORIES):
category = 'general'
cgi_data = cgi.FieldStorage()
if len(cgi_data.keys()):
if cgi_data.has_key('VARHELP'):
FormatOptionHelp(doc, cgi_data['VARHELP'].value, lst)
print doc.Format(bgcolor="#ffffff")
return
if not cgi_data.has_key('adminpw'):
AddErrorMessage(doc,
'Error: You must supply the admin password to'
' change options.')
else:
try:
lst.ConfirmAdminPassword(cgi_data['adminpw'].value)
ChangeOptions(lst, category, cgi_data, doc)
# Yuck. This shouldn't need to be here.
if not lst.digestable and not lst.nondigestable:
lst.nondigestable = 1
except mm_err.MMBadPasswordError:
AddErrorMessage(doc, 'Error: Incorrect admin password.')
if not lst.digestable and len(lst.digest_members):
AddErrorMessage(doc,
'Warning: you have digest members,'
' but digests are turned off.'
' Those people will not receive mail.')
if not lst.nondigestable and len(lst.members):
AddErrorMessage(doc,
'Warning: you have lst members,'
' but non-digestified mail is turned'
' off. They will receive mail until'
' you fix this problem.')
if len(cgi_data.keys()):
if (cgi_data.has_key('bounce_matching_headers')):
try:
pairs = lst.parse_matching_header_opt()
except mm_err.MMBadConfigError, line:
AddErrorMessage(doc,
'Warning: bad matching-header line'
' (does it have the colon?)
',
line)
FormatConfiguration(doc, lst, category, category_suffix)
print doc.Format(bgcolor="#ffffff")
finally:
lst.Unlock()
# Form Production:
def FormatAdminOverview(error=None):
"Present a general welcome and itemize the (public) lists."
doc = Document()
legend = "%s maillists - Admin Links" % mm_cfg.DEFAULT_HOST_NAME
doc.SetTitle(legend)
table = Table(border=0, width="100%")
table.AddRow([Center(Header(2, legend))])
table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0,
colspan=2, bgcolor="#99ccff")
# XXX We need a portable way to determine the host by which we are being
# visited! An absolute URL would do...
if os.environ.has_key('HTTP_HOST'):
http_host = os.environ['HTTP_HOST']
else:
http_host = None
advertised = []
names = mm_utils.list_names()
names.sort()
for n in names:
l = maillist.MailList(n)
l.Unlock()
if l.advertised:
if (http_host
and (string.find(http_host, l.host_name) == -1
and string.find(l.host_name, http_host) == -1)):
# List is for different identity for this host - skip it.
continue
else:
advertised.append(l)
if error:
greeting = FontAttr(error, color="ff5060", size="+1")
else:
greeting = "Welcome!"
if not advertised:
welcome_items = (greeting,
""
" There currently are no publicly-advertised ",
Link(mm_cfg.MAILMAN_URL, "mailman"),
" maillists on %s." % mm_cfg.DEFAULT_HOST_NAME,
)
else:
welcome_items = (
greeting,
"
"
" Below is the collection of publicly-advertised ",
Link(mm_cfg.MAILMAN_URL, "mailman"),
" maillists on %s." % mm_cfg.DEFAULT_HOST_NAME,
(' Click on a list name to visit the configuration pages'
' for that list.'
)
)
welcome_items = (welcome_items +
(" To visit the administrators configuration page for"
" an unadvertised list, open a URL similar to this"
+
(" one, but with a '/' and the %slist name appended.
"
% ((error and "the right ") or ""))
+
" General list information can be found at ",
Link(os.path.join(mm_cfg.DEFAULT_URL, "listinfo/"),
"the maillist overview page"),
"."
"
(Send questions and comments to ",
Link("mailto:%s" % mm_cfg.MAILMAN_OWNER,
mm_cfg.MAILMAN_OWNER),
".)
"
)
)
table.AddRow([apply(Container, welcome_items)])
table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
if advertised:
table.AddRow([Italic("List"), Italic("Description")])
for l in advertised:
table.AddRow([Link(l.GetScriptURL('admin'), Bold(l.real_name)),
l.description])
doc.AddItem(table)
print doc.Format(bgcolor="#ffffff")
def FormatConfiguration(doc, lst, category, category_suffix):
"""Produce the overall doc, *except* any processing error messages."""
for k, v in CATEGORIES:
if k == category: label = v
doc.SetTitle('%s Administration' % lst.real_name)
doc.AddItem(Center(Header(2, ('%s Maillist Configuration - %s Section'
% (lst.real_name, label)))))
doc.AddItem('
')
links_table = Table(valign="top")
links_table.AddRow([Center(Bold("Configuration Categories")),
Center(Bold("Other Administrative Activities"))])
other_links = UnorderedList()
link = Link(lst.GetScriptURL('admindb'),
'Tend to pending administrative requests.')
other_links.AddItem(link)
link = Link(lst.GetScriptURL('listinfo'),
'Go to the general list information page.')
other_links.AddItem(link)
link = Link(lst.GetScriptURL('edithtml'),
'Edit the HTML for the public list pages.')
other_links.AddItem(link)
these_links = UnorderedList()
url = lst.GetScriptURL('admin')
for k, v in CATEGORIES:
if k == category:
these_links.AddItem(" => " + v + " <= ")
else:
these_links.AddItem(Link(os.path.join(url, k), v))
links_table.AddRow([these_links, other_links])
links_table.AddRowInfo(max(links_table.GetCurrentRowIndex(), 0),
valign="top")
doc.AddItem(links_table)
doc.AddItem('
')
if category_suffix:
form = Form(os.path.join(lst.GetScriptURL('admin'), category))
else:
form = Form(lst.GetScriptURL('admin'))
doc.AddItem(form)
form.AddItem("Make your changes, below, and then submit it all at the"
" bottom. (You can also change your password there,"
" as well.)")
form.AddItem(FormatOptionsSection(category, lst))
form.AddItem(Center(FormatPasswordStuff()))
form.AddItem(lst.GetMailmanFooter())
def FormatOptionsSection(category, lst):
"""Produce the category-specific options table."""
if category == 'members':
# Special case for members section.
return FormatMembershipOptions(lst)
options = GetConfigOptions(lst, category)
big_table = Table(cellspacing=3, cellpadding=4)
# Get and portray the text label for the category.
for k, v in CATEGORIES:
if k == category: label = v
big_table.AddRow([Center(Header(2, label))])
big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 0,
colspan=2, bgcolor="#99ccff")
def ColHeader(big_table = big_table):
big_table.AddRow([Center(Bold('Description')), Center(Bold('Value'))])
big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 0,
width="15%")
big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0), 1,
width="85%")
did_col_header = 0
for item in options:
if type(item) == types.StringType:
# The very first banner option (string in an options list) is
# treated as a general description, while any others are
# treated as section headers - centered and italicized...
if did_col_header:
item = "
" + item + ""
big_table.AddRow([item])
big_table.AddCellInfo(max(big_table.GetCurrentRowIndex(), 0),
0, colspan=2)
if not did_col_header:
# Do col header after very first string descr, if any...
ColHeader()
did_col_header = 1
else:
if not did_col_header:
# ... but do col header before anything else.
ColHeader()
did_col_header = 1
AddOptionsTableItem(big_table, item, category, lst)
big_table.AddRow(['
'])
big_table.AddCellInfo(big_table.GetCurrentRowIndex(), 0, colspan=2)
return big_table
def AddOptionsTableItem(table, item, category, lst, nodetails=0):
"""Add a row to an options table with the item description and value."""
try:
got = GetItemCharacteristics(item)
varname, kind, params, dependancies, descr, elaboration = got
except ValueError, msg:
lst.LogMsg("error", "admin: %s", msg)
return Italic("")
descr = GetItemGuiDescr(lst, category, varname, descr,
elaboration, nodetails)
val = GetItemGuiValue(lst, kind, varname, params)
table.AddRow([descr, val])
table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1,
bgcolor="#cccccc")
table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0,
bgcolor="#cccccc")
def FormatOptionHelp(doc, varref, lst):
item = bad = None
reflist = string.split(varref, '/')
if len(reflist) == 2:
category, varname = reflist
options = GetConfigOptions(lst, category)
for i in options:
if i and i[0] == varname:
item = i
break
if not item:
bad = ("Option %s/%s not found. %s"
% (category, varname, os.environ['PATH_INFO']))
else:
try:
got = GetItemCharacteristics(item)
varname, kind, params, dependancies, descr, elaboration = got
except ValueError, msg:
bad = msg
if not bad and not elaboration:
bad = "Option %s has no extended help." % varname
if bad:
AddErrorMessage(doc, bad)
return
header = Table(width="100%")
legend = ('%s Maillist Configuration Help
%s Option'
% (lst.real_name, varname))
header.AddRow([Center(Header(3, legend))])
header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0,
colspan=2, bgcolor="#99ccff")
doc.SetTitle("Mailman %s List Option Help" % varname)
doc.AddItem(header)
doc.AddItem("%s (%s): %s" % (varname, category, item[4]))
doc.AddItem("%s
" % item[5])
form = Form(os.path.join(lst.GetScriptURL('admin'), category))
valtab = Table(cellspacing=3, cellpadding=4)
AddOptionsTableItem(valtab, item, category, lst, nodetails=1)
form.AddItem(valtab)
# XXX I don't think we want to be able to set options from two places,
# since they'll go out of sync.
#form.AddItem(Center(FormatPasswordStuff()))
doc.AddItem(Center(form))
def GetItemCharacteristics(table_entry):
"""Break out the components of an item description from its table entry:
0 option-var name
1 type
2 entry size
3 ?dependancies?
4 Brief description
5 Optional description elaboration"""
if len(table_entry) == 5:
elaboration = None
varname, kind, params, dependancies, descr = table_entry
elif len(table_entry) == 6:
varname, kind, params, dependancies, descr, elaboration = table_entry
else:
raise ValueError, ("Badly formed options entry:\n %s"
% table_entry)
return (varname, kind, params, dependancies, descr, elaboration)
def GetItemGuiValue(lst, kind, varname, params):
"""Return a representation of an item's settings."""
if kind == mm_cfg.Radio or kind == mm_cfg.Toggle:
return RadioButtonArray(varname, params, getattr(lst, varname))
elif (kind == mm_cfg.String or kind == mm_cfg.Email or
kind == mm_cfg.Host or kind == mm_cfg.Number):
return TextBox(varname, getattr(lst, varname), params)
elif kind == mm_cfg.Text:
if params:
r, c = params
else:
r, c = None, None
val = getattr(lst, varname)
if not val:
val = ''
return TextArea(varname, val, r, c)
elif kind == mm_cfg.EmailList:
if params:
r, c = params
else:
r, c = None, None
res = string.join(getattr(lst, varname), '\n')
return TextArea(varname, res, r, c, wrap='off')
def GetItemGuiDescr(lst, category, varname, descr, elaboration, nodetails):
"""Return a representation of an item's description, with link to
elaboration if any."""
descr = '
' + descr
if not nodetails and elaboration:
if len(list_info) == 1:
ref = list_name + "/"
else:
ref = ""
ref = ref + '?VARHELP=' + category + "/" + varname
descr = Container(descr,
Link(ref, " (Details)", target="MMHelp"),
"
")
else:
descr = descr + ""
return descr
def FormatMembershipOptions(lst):
container = Container()
header = Table(width="100%")
header.AddRow([Center(Header(2, "Membership Management"))])
header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0,
colspan=2, bgcolor="#99ccff")
header.AddRow([Bold("Subscribe and Unsubscribe Members")])
header.AddCellInfo(max(header.GetCurrentRowIndex(), 0), 0,
colspan=2, bgcolor ="#FFF0D0")
container.AddItem(header)
container.AddItem('Mass Subscribe Members
')
container.AddItem('Enter one email address per line
')
container.AddItem(TextArea(name='subscribees', rows=20,cols=60,wrap=None))
container.AddItem('To Unsubscribe Members...
')
container.AddItem("""
To unsubscribe members you must use your admin password in place of the
user's password on the user's edit-options page. Visit their
edit-options page (via the roster page) and do the
unsubscribe procedure, providing the admin password instead of the
user's password.
(Note that you can, alternately, set the subscriber's no-delivery
option to inhibit delivery of their messages, if you want to only
temporarily disable their delivery.)
"""
% lst.GetScriptURL('roster'))
return container
def FormatPasswordStuff():
password_submit = Table(bgcolor="#99cccc",
border=0, cellspacing=0, cellpadding=2)
password_submit.AddRow([Center(Bold('To Submit Your Changes'))])
password_submit.AddCellInfo(password_submit.GetCurrentRowIndex(), 0,
colspan=2)
password_submit.AddRow(['
Enter the administrator '
'password:
',
PasswordBox('adminpw')])
password_submit.AddRow(['And...
',
Bold(SubmitButton('submit', 'Submit Changes'))])
change_pw_table = Table(bgcolor="#cccccc", border=0,
cellspacing=0, cellpadding=2)
change_pw_table.AddRow([Bold(Center('To Change The Administrator'
' Password'))])
change_pw_table.AddCellInfo(change_pw_table.GetCurrentRowIndex(), 0,
colspan=2)
change_pw_table.AddRow(['Enter the new '
'password:
',
PasswordBox('newpw')])
change_pw_table.AddRow(['And also confirm it:
',
PasswordBox('confirmpw')])
password_stuff = Table(bgcolor="#99cccc")
password_stuff.AddRow([password_submit, change_pw_table])
return password_stuff
# XXX klm - looks like turn_on_moderation is orphaned.
turn_on_moderation = 0
# Options processing
def GetValidValue(lst, prop, my_type, val, dependant):
if my_type == mm_cfg.Radio or my_type == mm_cfg.Toggle:
if type(val) <> types.IntType:
try:
# XXX Security!?
val = eval(val)
except:
pass
# Don't know what to do here...
return val
elif my_type == mm_cfg.String or my_type == mm_cfg.Text:
return val
elif my_type == mm_cfg.Email:
try:
valid = mm_utils.ValidEmail(val)
if valid:
return val
except:
pass
# Revert to the old value.
return getattr(lst, prop)
elif my_type == mm_cfg.EmailList:
def SafeValidAddr(addr):
import mm_utils
try:
valid = mm_utils.ValidEmail(addr)
if valid:
return 1
else:
return 0
except:
return 0
val = filter(SafeValidAddr,
map(string.strip, string.split(val, '\n')))
if dependant and len(val):
# Wait till we've set everything to turn it on,
# as we don't want to clobber our special case.
# XXX klm - looks like turn_on_moderation is orphaned?
turn_on_moderation = 1
return val
elif my_type == mm_cfg.Host:
return val
##
## This code is sendmail dependant, so we'll just live w/o
## the error checking for now.
##
## # Shouldn't have to read in the whole file.
## file = open('/etc/sendmail.cf', 'r')
## lines = string.split(file.read(), '\n')
## file.close()
## def ConfirmCWEntry(item):
## return item[0:2] == 'Cw'
## lines = filter(ConfirmCWEntry, lines)
## if not len(lines):
## # Revert to the old value.
## return getattr(list, prop)
## for line in lines:
## if string.lower(string.strip(line[2:])) == string.lower(val):
## return val
## return getattr(list, prop)
elif my_type == mm_cfg.Number:
try:
num = eval(val)
if num < 0:
return getattr(lst, prop)
return num
except:
return getattr(lst, prop)
else:
# Should never get here...
return val
def ChangeOptions(lst, category, cgi_info, document):
dirty = 0
if category != 'members':
opt_list = GetConfigOptions(lst, category)
for item in opt_list:
if len(item) < 5:
continue
property, kind, args, deps, desc = (item[0], item[1], item[2],
item[3], item[4])
if not cgi_info.has_key(property):
if (kind <> mm_cfg.Text and
kind <> mm_cfg.String and
kind <> mm_cfg.EmailList):
continue
else:
val = ''
else:
val = cgi_info[property].value
value = GetValidValue(lst, property, kind, val, deps)
if getattr(lst, property) != value:
setattr(lst, property, value)
dirty = 1
if cgi_info.has_key('subscribees'):
name_text = cgi_info['subscribees'].value
names = string.split(name_text, '\r\n')
for new_name in names:
try:
#FIXME: The admin needs to be able to specify subscribe options
lst.AddMember(new_name, (mm_utils.GetRandomSeed() +
mm_utils.GetRandomSeed()))
dirty = 1
#FIXME: Give some sort of an indication of which names didn't work,
# and why they didn't work...
except:
pass
if cgi_info.has_key('newpw'):
if cgi_info.has_key('confirmpw'):
new = cgi_info['newpw'].value
confirm = cgi_info['confirmpw'].value
if new == confirm:
lst.password = crypt.crypt(new, mm_utils.GetRandomSeed())
dirty = 1
else:
m = 'Error: Passwords did not match.'
document.AddItem(
Header(3, Italic(FontAttr(m, color="ff5060"))))
else:
m = 'Error: You must type in your new password twice.'
document.AddItem(
Header(3, Italic(FontAttr(m, color="ff5060"))))
if dirty:
lst.Save()
def AddErrorMessage(doc, errmsg, *args):
doc.AddItem(Header(3, Italic(FontAttr(errmsg % args,
color="#ff66cc"))))
_config_info = None
def GetConfigOptions(lst, category):
global _config_info
if _config_info == None:
_config_info = lst.GetConfigInfo()
return _config_info[category]
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print "Interrupted!"
raise SystemExit, 0
except mm_err.MMUnknownListError, msg:
FormatAdminOverview(error="List %s not found." % list_name)
except:
print "Content-type: text/html\n"
print "We're sorry, we hit a bug!
\n"
print "If you would like to help us identify the problem, please "
print "email a copy of this page to the webmaster for this site"
print 'with a description of what happened. Thanks!'
print "\n"
try:
import traceback
sys.stderr = sys.stdout
traceback.print_exc()
except:
print "[failed to get traceback]"
print "\n\n
"