aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Cgi
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/Cgi')
-rw-r--r--Mailman/Cgi/.cvsignore1
-rw-r--r--Mailman/Cgi/Auth.py59
-rw-r--r--Mailman/Cgi/Makefile.in71
-rw-r--r--Mailman/Cgi/__init__.py15
-rw-r--r--Mailman/Cgi/admin.py1407
-rw-r--r--Mailman/Cgi/admindb.py769
-rw-r--r--Mailman/Cgi/confirm.py791
-rw-r--r--Mailman/Cgi/create.py410
-rw-r--r--Mailman/Cgi/edithtml.py170
-rw-r--r--Mailman/Cgi/listinfo.py206
-rw-r--r--Mailman/Cgi/options.py950
-rw-r--r--Mailman/Cgi/private.py162
-rw-r--r--Mailman/Cgi/rmlist.py242
-rw-r--r--Mailman/Cgi/roster.py129
-rw-r--r--Mailman/Cgi/subscribe.py276
15 files changed, 5658 insertions, 0 deletions
diff --git a/Mailman/Cgi/.cvsignore b/Mailman/Cgi/.cvsignore
new file mode 100644
index 00000000..f3c7a7c5
--- /dev/null
+++ b/Mailman/Cgi/.cvsignore
@@ -0,0 +1 @@
+Makefile
diff --git a/Mailman/Cgi/Auth.py b/Mailman/Cgi/Auth.py
new file mode 100644
index 00000000..58640663
--- /dev/null
+++ b/Mailman/Cgi/Auth.py
@@ -0,0 +1,59 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Common routines for logging in and logging out of the list administrator
+and list moderator interface.
+"""
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import Errors
+from Mailman.htmlformat import FontAttr
+from Mailman.i18n import _
+
+
+
+class NotLoggedInError(Exception):
+ """Exception raised when no matching admin cookie was found."""
+ def __init__(self, message):
+ Exception.__init__(self, message)
+ self.message = message
+
+
+
+def loginpage(mlist, scriptname, msg='', frontpage=None):
+ url = mlist.GetScriptURL(scriptname)
+ if frontpage:
+ actionurl = url
+ else:
+ actionurl = Utils.GetRequestURI(url)
+ if msg:
+ msg = FontAttr(msg, color='#ff0000', size='+1').Format()
+ if scriptname == 'admindb':
+ who = _('Moderator')
+ else:
+ who = _('Administrator')
+ # Language stuff
+ charset = Utils.GetCharSet(mlist.preferred_language)
+ print 'Content-type: text/html; charset=' + charset + '\n\n'
+ print Utils.maketext(
+ 'admlogin.html',
+ {'listname': mlist.real_name,
+ 'path' : actionurl,
+ 'message' : msg,
+ 'who' : who,
+ }, mlist=mlist)
+ print mlist.GetMailmanFooter()
diff --git a/Mailman/Cgi/Makefile.in b/Mailman/Cgi/Makefile.in
new file mode 100644
index 00000000..a613c2b0
--- /dev/null
+++ b/Mailman/Cgi/Makefile.in
@@ -0,0 +1,71 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+# NOTE: Makefile.in is converted into Makefile by the configure script
+# in the parent directory. Once configure has run, you can recreate
+# the Makefile by running just config.status.
+
+# Variables set by configure
+
+VPATH= @srcdir@
+srcdir= @srcdir@
+bindir= @bindir@
+prefix= @prefix@
+exec_prefix= @exec_prefix@
+
+CC= @CC@
+CHMOD= @CHMOD@
+INSTALL= @INSTALL@
+
+DEFS= @DEFS@
+
+# Customizable but not set by configure
+
+OPT= @OPT@
+CFLAGS= $(OPT) $(DEFS)
+PACKAGEDIR= $(prefix)/Mailman
+CGIDIR= $(PACKAGEDIR)/Cgi
+SHELL= /bin/sh
+
+CGI_MODULES= *.py
+
+
+# Modes for directories and executables created by the install
+# process. Default to group-writable directories but
+# user-only-writable for executables.
+DIRMODE= 775
+EXEMODE= 755
+FILEMODE= 644
+INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
+
+
+# Rules
+
+all:
+
+install:
+ for f in $(CGI_MODULES); \
+ do \
+ $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(CGIDIR); \
+ done
+
+finish:
+
+clean:
+
+distclean:
+ -rm *.pyc
+ -rm Makefile
diff --git a/Mailman/Cgi/__init__.py b/Mailman/Cgi/__init__.py
new file mode 100644
index 00000000..2cbbabb1
--- /dev/null
+++ b/Mailman/Cgi/__init__.py
@@ -0,0 +1,15 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py
new file mode 100644
index 00000000..49c6efbf
--- /dev/null
+++ b/Mailman/Cgi/admin.py
@@ -0,0 +1,1407 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Process and produce the list-administration options forms.
+
+"""
+
+# For Python 2.1.x compatibility
+from __future__ import nested_scopes
+
+import sys
+import os
+import re
+import cgi
+import sha
+import urllib
+import signal
+from types import *
+from string import lowercase, digits
+
+from email.Utils import unquote, parseaddr, formataddr
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import MemberAdaptor
+from Mailman import i18n
+from Mailman.UserDesc import UserDesc
+from Mailman.htmlformat import *
+from Mailman.Cgi import Auth
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+NL = '\n'
+OPTCOLUMNS = 11
+
+
+
+def main():
+ # Try to find out which list is being administered
+ parts = Utils.GetPathPieces()
+ if not parts:
+ # None, so just do the admin overview and be done with it
+ admin_overview()
+ return
+ # Get the list object
+ listname = parts[0].lower()
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ admin_overview(_('No such list <em>%(safelistname)s</em>'))
+ syslog('error', 'admin.py access for non-existent list: %s',
+ listname)
+ return
+ # Now that we know what list has been requested, all subsequent admin
+ # pages are shown in that list's preferred language.
+ i18n.set_language(mlist.preferred_language)
+ # If the user is not authenticated, we're done.
+ cgidata = cgi.FieldStorage(keep_blank_values=1)
+
+ if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
+ mm_cfg.AuthSiteAdmin),
+ cgidata.getvalue('adminpw', '')):
+ if cgidata.has_key('adminpw'):
+ # This is a re-authorization attempt
+ msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
+ else:
+ msg = ''
+ Auth.loginpage(mlist, 'admin', msg=msg)
+ return
+
+ # Which subcategory was requested? Default is `general'
+ if len(parts) == 1:
+ category = 'general'
+ subcat = None
+ elif len(parts) == 2:
+ category = parts[1]
+ subcat = None
+ else:
+ category = parts[1]
+ subcat = parts[2]
+
+ # Is this a log-out request?
+ if category == 'logout':
+ print mlist.ZapCookie(mm_cfg.AuthListAdmin)
+ Auth.loginpage(mlist, 'admin', frontpage=1)
+ return
+
+ # Sanity check
+ if category not in mlist.GetConfigCategories().keys():
+ category = 'general'
+
+ # Is the request for variable details?
+ varhelp = None
+ qsenviron = os.environ.get('QUERY_STRING')
+ parsedqs = None
+ if qsenviron:
+ parsedqs = cgi.parse_qs(qsenviron)
+ if cgidata.has_key('VARHELP'):
+ varhelp = cgidata.getvalue('VARHELP')
+ elif parsedqs:
+ # POST methods, even if their actions have a query string, don't get
+ # put into FieldStorage's keys :-(
+ qs = parsedqs.get('VARHELP')
+ if qs and isinstance(qs, ListType):
+ varhelp = qs[0]
+ if varhelp:
+ option_help(mlist, varhelp)
+ return
+
+ # The html page document
+ doc = Document()
+ doc.set_language(mlist.preferred_language)
+
+ # From this point on, the MailList object must be locked. However, we
+ # must release the lock no matter how we exit. try/finally isn't enough,
+ # because of this scenario: user hits the admin page which may take a long
+ # time to render; user gets bored and hits the browser's STOP button;
+ # browser shuts down socket; server tries to write to broken socket and
+ # gets a SIGPIPE. Under Apache 1.3/mod_cgi, Apache catches this SIGPIPE
+ # (I presume it is buffering output from the cgi script), then turns
+ # around and SIGTERMs the cgi process. Apache waits three seconds and
+ # then SIGKILLs the cgi process. We /must/ catch the SIGTERM and do the
+ # most reasonable thing we can in as short a time period as possible. If
+ # we get the SIGKILL we're screwed (because it's uncatchable and we'll
+ # have no opportunity to clean up after ourselves).
+ #
+ # This signal handler catches the SIGTERM, unlocks the list, and then
+ # exits the process. The effect of this is that the changes made to the
+ # MailList object will be aborted, which seems like the only sensible
+ # semantics.
+ #
+ # BAW: This may not be portable to other web servers or cgi execution
+ # models.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ # Make sure the list gets unlocked...
+ mlist.Unlock()
+ # ...and ensure we exit, otherwise race conditions could cause us to
+ # enter MailList.Save() while we're in the unlocked state, and that
+ # could be bad!
+ sys.exit(0)
+
+ mlist.Lock()
+ try:
+ # Install the emergency shutdown signal handler
+ signal.signal(signal.SIGTERM, sigterm_handler)
+
+ if cgidata.keys():
+ # There are options to change
+ change_options(mlist, category, subcat, cgidata, doc)
+ # Let the list sanity check the changed values
+ mlist.CheckValues()
+ # Additional sanity checks
+ if not mlist.digestable and not mlist.nondigestable:
+ doc.addError(
+ _('''You have turned off delivery of both digest and
+ non-digest messages. This is an incompatible state of
+ affairs. You must turn on either digest delivery or
+ non-digest delivery or your mailing list will basically be
+ unusable.'''), tag=_('Warning: '))
+
+ if not mlist.digestable and mlist.getDigestMemberKeys():
+ doc.addError(
+ _('''You have digest members, but digests are turned
+ off. Those people will not receive mail.'''),
+ tag=_('Warning: '))
+ if not mlist.nondigestable and mlist.getRegularMemberKeys():
+ doc.addError(
+ _('''You have regular list members but non-digestified mail is
+ turned off. They will receive mail until you fix this
+ problem.'''), tag=_('Warning: '))
+ # Glom up the results page and print it out
+ show_results(mlist, doc, category, subcat, cgidata)
+ print doc.Format()
+ mlist.Save()
+ finally:
+ # Now be sure to unlock the list. It's okay if we get a signal here
+ # because essentially, the signal handler will do the same thing. And
+ # unlocking is unconditional, so it's not an error if we unlock while
+ # we're already unlocked.
+ mlist.Unlock()
+
+
+
+def admin_overview(msg=''):
+ # Show the administrative overview page, with the list of all the lists on
+ # this host. msg is an optional error message to display at the top of
+ # the page.
+ #
+ # This page should be displayed in the server's default language, which
+ # should have already been set.
+ hostname = Utils.get_domain()
+ legend = _('%(hostname)s mailing lists - Admin Links')
+ # The html `document'
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+ doc.SetTitle(legend)
+ # The table that will hold everything
+ table = Table(border=0, width="100%")
+ table.AddRow([Center(Header(2, legend))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ # Skip any mailing list that isn't advertised.
+ advertised = []
+ listnames = Utils.list_names()
+ listnames.sort()
+
+ for name in listnames:
+ mlist = MailList.MailList(name, lock=0)
+ if mlist.advertised:
+ if mm_cfg.VIRTUAL_HOST_OVERVIEW and \
+ mlist.web_page_url.find(hostname) == -1:
+ # List is for different identity of this host - skip it.
+ continue
+ else:
+ advertised.append(mlist)
+
+ # Greeting depends on whether there was an error or not
+ if msg:
+ greeting = FontAttr(msg, color="ff5060", size="+1")
+ else:
+ greeting = _("Welcome!")
+
+ welcome = []
+ mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
+ if not advertised:
+ welcome.extend([
+ greeting,
+ _('''<p>There currently are no publicly-advertised %(mailmanlink)s
+ mailing lists on %(hostname)s.'''),
+ ])
+ else:
+ welcome.extend([
+ greeting,
+ _('''<p>Below is the collection of publicly-advertised
+ %(mailmanlink)s mailing lists on %(hostname)s. Click on a list
+ name to visit the configuration pages for that list.'''),
+ ])
+
+ creatorurl = Utils.ScriptURL('create')
+ mailman_owner = Utils.get_site_email()
+ extra = msg and _('right ') or ''
+ welcome.extend([
+ _('''To visit the administrators configuration page for an
+ unadvertised list, open a URL similar to this one, but with a '/' and
+ the %(extra)slist name appended. If you have the proper authority,
+ you can also <a href="%(creatorurl)s">create a new mailing list</a>.
+
+ <p>General list information can be found at '''),
+ Link(Utils.ScriptURL('listinfo'),
+ _('the mailing list overview page')),
+ '.',
+ _('<p>(Send questions and comments to '),
+ Link('mailto:%s' % mailman_owner, mailman_owner),
+ '.)<p>',
+ ])
+
+ table.AddRow([Container(*welcome)])
+ table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
+
+ if advertised:
+ table.AddRow(['&nbsp;', '&nbsp;'])
+ table.AddRow([Bold(FontAttr(_('List'), size='+2')),
+ Bold(FontAttr(_('Description'), size='+2'))
+ ])
+ highlight = 1
+ for mlist in advertised:
+ table.AddRow(
+ [Link(mlist.GetScriptURL('admin'), Bold(mlist.real_name)),
+ mlist.description or Italic(_('[no description available]'))])
+ if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR:
+ table.AddRowInfo(table.GetCurrentRowIndex(),
+ bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR)
+ highlight = not highlight
+
+ doc.AddItem(table)
+ doc.AddItem('<hr>')
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+
+
+
+def option_help(mlist, varhelp):
+ # The html page document
+ doc = Document()
+ doc.set_language(mlist.preferred_language)
+ # Find out which category and variable help is being requested for.
+ item = None
+ reflist = varhelp.split('/')
+ if len(reflist) >= 2:
+ category = subcat = None
+ if len(reflist) == 2:
+ category, varname = reflist
+ elif len(reflist) == 3:
+ category, subcat, varname = reflist
+ options = mlist.GetConfigInfo(category, subcat)
+ for i in options:
+ if i and i[0] == varname:
+ item = i
+ break
+ # Print an error message if we couldn't find a valid one
+ if not item:
+ bad = _('No valid variable name found.')
+ doc.addError(bad)
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ return
+ # Get the details about the variable
+ varname, kind, params, dependancies, description, elaboration = \
+ get_item_characteristics(item)
+ # Set up the document
+ realname = mlist.real_name
+ legend = _("""%(realname)s Mailing list Configuration Help
+ <br><em>%(varname)s</em> Option""")
+
+ header = Table(width='100%')
+ header.AddRow([Center(Header(3, legend))])
+ header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ doc.SetTitle(_("Mailman %(varname)s List Option Help"))
+ doc.AddItem(header)
+ doc.AddItem("<b>%s</b> (%s): %s<p>" % (varname, category, description))
+ if elaboration:
+ doc.AddItem("%s<p>" % elaboration)
+
+ if subcat:
+ url = '%s/%s/%s' % (mlist.GetScriptURL('admin'), category, subcat)
+ else:
+ url = '%s/%s' % (mlist.GetScriptURL('admin'), category)
+ form = Form(url)
+ valtab = Table(cellspacing=3, cellpadding=4, width='100%')
+ add_options_table_item(mlist, category, subcat, valtab, item, detailsp=0)
+ form.AddItem(valtab)
+ form.AddItem('<p>')
+ form.AddItem(Center(submit_button()))
+ doc.AddItem(Center(form))
+
+ doc.AddItem(_("""<em><strong>Warning:</strong> changing this option here
+ could cause other screens to be out-of-sync. Be sure to reload any other
+ pages that are displaying this option for this mailing list. You can also
+ """))
+
+ adminurl = mlist.GetScriptURL('admin')
+ if subcat:
+ url = '%s/%s/%s' % (adminurl, category, subcat)
+ else:
+ url = '%s/%s' % (adminurl, category)
+ categoryname = mlist.GetConfigCategories()[category][0]
+ doc.AddItem(Link(url, _('return to the %(categoryname)s options page.')))
+ doc.AddItem('</em>')
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+
+
+
+def show_results(mlist, doc, category, subcat, cgidata):
+ # Produce the results page
+ adminurl = mlist.GetScriptURL('admin')
+ categories = mlist.GetConfigCategories()
+ label = _(categories[category][0])
+
+ # Set up the document's headers
+ realname = mlist.real_name
+ doc.SetTitle(_('%(realname)s Administration (%(label)s)'))
+ doc.AddItem(Center(Header(2, _(
+ '%(realname)s mailing list administration<br>%(label)s Section'))))
+ doc.AddItem('<hr>')
+ # Now we need to craft the form that will be submitted, which will contain
+ # all the variable settings, etc. This is a bit of a kludge because we
+ # know that the autoreply and members categories supports file uploads.
+ encoding = None
+ if category in ('autoreply', 'members'):
+ encoding = 'multipart/form-data'
+ if subcat:
+ form = Form('%s/%s/%s' % (adminurl, category, subcat),
+ encoding=encoding)
+ else:
+ form = Form('%s/%s' % (adminurl, category), encoding=encoding)
+ # This holds the two columns of links
+ linktable = Table(valign='top', width='100%')
+ linktable.AddRow([Center(Bold(_("Configuration Categories"))),
+ Center(Bold(_("Other Administrative Activities")))])
+ # The `other links' are stuff in the right column.
+ otherlinks = UnorderedList()
+ otherlinks.AddItem(Link(mlist.GetScriptURL('admindb'),
+ _('Tend to pending moderator requests')))
+ otherlinks.AddItem(Link(mlist.GetScriptURL('listinfo'),
+ _('Go to the general list information page')))
+ otherlinks.AddItem(Link(mlist.GetScriptURL('edithtml'),
+ _('Edit the public HTML pages')))
+ otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(),
+ _('Go to list archives')).Format() +
+ '<br>&nbsp;<br>')
+ # We do not allow through-the-web deletion of the site list!
+ if mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS and \
+ mlist.internal_name() <> mm_cfg.MAILMAN_SITE_LIST:
+ otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'),
+ _('Delete this mailing list')).Format() +
+ _(' (requires confirmation)<br>&nbsp;<br>'))
+ otherlinks.AddItem(Link('%s/logout' % adminurl,
+ # BAW: What I really want is a blank line, but
+ # adding an &nbsp; won't do it because of the
+ # bullet added to the list item.
+ '<FONT SIZE="+2"><b>%s</b></FONT>' %
+ _('Logout')))
+ # These are links to other categories and live in the left column
+ categorylinks_1 = categorylinks = UnorderedList()
+ categorylinks_2 = ''
+ categorykeys = categories.keys()
+ half = len(categorykeys) / 2
+ counter = 0
+ subcat = None
+ for k in categorykeys:
+ label = _(categories[k][0])
+ url = '%s/%s' % (adminurl, k)
+ if k == category:
+ # Handle subcategories
+ subcats = mlist.GetConfigSubCategories(k)
+ if subcats:
+ subcat = Utils.GetPathPieces()[-1]
+ for k, v in subcats:
+ if k == subcat:
+ break
+ else:
+ # The first subcategory in the list is the default
+ subcat = subcats[0][0]
+ subcat_items = []
+ for sub, text in subcats:
+ if sub == subcat:
+ text = Bold('[%s]' % text).Format()
+ subcat_items.append(Link(url + '/' + sub, text))
+ categorylinks.AddItem(
+ Bold(label).Format() +
+ UnorderedList(*subcat_items).Format())
+ else:
+ categorylinks.AddItem(Link(url, Bold('[%s]' % label)))
+ else:
+ categorylinks.AddItem(Link(url, label))
+ counter += 1
+ if counter >= half:
+ categorylinks_2 = categorylinks = UnorderedList()
+ counter = -len(categorykeys)
+ # Make the emergency stop switch a rude solo light
+ etable = Table()
+ # Add all the links to the links table...
+ etable.AddRow([categorylinks_1, categorylinks_2])
+ etable.AddRowInfo(etable.GetCurrentRowIndex(), valign='top')
+ if mlist.emergency:
+ label = _('Emergency moderation of all list traffic is enabled')
+ etable.AddRow([Center(
+ Link('?VARHELP=general/emergency', Bold(label)))])
+ color = mm_cfg.WEB_ERROR_COLOR
+ etable.AddCellInfo(etable.GetCurrentRowIndex(), 0,
+ colspan=2, bgcolor=color)
+ linktable.AddRow([etable, otherlinks])
+ # ...and add the links table to the document.
+ form.AddItem(linktable)
+ form.AddItem('<hr>')
+ form.AddItem(
+ _('''Make your changes in the following section, then submit them
+ using the <em>Submit Your Changes</em> button below.''')
+ + '<p>')
+
+ # The members and passwords categories are special in that they aren't
+ # defined in terms of gui elements. Create those pages here.
+ if category == 'members':
+ # Figure out which subcategory we should display
+ subcat = Utils.GetPathPieces()[-1]
+ if subcat not in ('list', 'add', 'remove'):
+ subcat = 'list'
+ # Add member category specific tables
+ form.AddItem(membership_options(mlist, subcat, cgidata, doc, form))
+ form.AddItem(Center(submit_button('setmemberopts_btn')))
+ # In "list" subcategory, we can also search for members
+ if subcat == 'list':
+ form.AddItem('<hr>\n')
+ table = Table(width='100%')
+ table.AddRow([Center(Header(2, _('Additional Member Tasks')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ # Add a blank separator row
+ table.AddRow(['&nbsp;', '&nbsp;'])
+ # Add a section to set the moderation bit for all members
+ table.AddRow([_("""<li>Set everyone's moderation bit, including
+ those members not currently visible""")])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([RadioButtonArray('allmodbit_val',
+ (_('Off'), _('On')),
+ mlist.default_member_moderation),
+ SubmitButton('allmodbit_btn', _('Set'))])
+ form.AddItem(table)
+ elif category == 'passwords':
+ form.AddItem(Center(password_inputs(mlist)))
+ form.AddItem(Center(submit_button()))
+ else:
+ form.AddItem(show_variables(mlist, category, subcat, cgidata, doc))
+ form.AddItem(Center(submit_button()))
+ # And add the form
+ doc.AddItem(form)
+ doc.AddItem(mlist.GetMailmanFooter())
+
+
+
+def show_variables(mlist, category, subcat, cgidata, doc):
+ options = mlist.GetConfigInfo(category, subcat)
+
+ # The table containing the results
+ table = Table(cellspacing=3, cellpadding=4, width='100%')
+
+ # Get and portray the text label for the category.
+ categories = mlist.GetConfigCategories()
+ label = _(categories[category][0])
+
+ table.AddRow([Center(Header(2, label))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ # The very first item in the config info will be treated as a general
+ # description if it is a string
+ description = options[0]
+ if isinstance(description, StringType):
+ table.AddRow([description])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ options = options[1:]
+
+ if not options:
+ return table
+
+ # Add the global column headers
+ table.AddRow([Center(Bold(_('Description'))),
+ Center(Bold(_('Value')))])
+ table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0,
+ width='15%')
+ table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 1,
+ width='85%')
+
+ for item in options:
+ if type(item) == 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...
+ table.AddRow([Center(Italic(item))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ else:
+ add_options_table_item(mlist, category, subcat, table, item)
+ table.AddRow(['<br>'])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ return table
+
+
+
+def add_options_table_item(mlist, category, subcat, table, item, detailsp=1):
+ # Add a row to an options table with the item description and value.
+ varname, kind, params, extra, descr, elaboration = \
+ get_item_characteristics(item)
+ if elaboration is None:
+ elaboration = descr
+ descr = get_item_gui_description(mlist, category, subcat,
+ varname, descr, elaboration, detailsp)
+ val = get_item_gui_value(mlist, category, kind, varname, params, extra)
+ table.AddRow([descr, val])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
+ table.AddCellInfo(table.GetCurrentRowIndex(), 1,
+ bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
+
+
+
+def get_item_characteristics(record):
+ # Break out the components of an item description from its description
+ # record:
+ #
+ # 0 -- option-var name
+ # 1 -- type
+ # 2 -- entry size
+ # 3 -- ?dependancies?
+ # 4 -- Brief description
+ # 5 -- Optional description elaboration
+ if len(record) == 5:
+ elaboration = None
+ varname, kind, params, dependancies, descr = record
+ elif len(record) == 6:
+ varname, kind, params, dependancies, descr, elaboration = record
+ else:
+ raise ValueError, _('Badly formed options entry:\n %(record)s')
+ return varname, kind, params, dependancies, descr, elaboration
+
+
+
+def get_item_gui_value(mlist, category, kind, varname, params, extra):
+ """Return a representation of an item's settings."""
+ # Give the category a chance to return the value for the variable
+ value = None
+ label, gui = mlist.GetConfigCategories()[category]
+ if hasattr(gui, 'getValue'):
+ value = gui.getValue(mlist, kind, varname, params)
+ # Filter out None, and volatile attributes
+ if value is None and not varname.startswith('_'):
+ value = getattr(mlist, varname)
+ # Now create the widget for this value
+ if kind == mm_cfg.Radio or kind == mm_cfg.Toggle:
+ # If we are returning the option for subscribe policy and this site
+ # doesn't allow open subscribes, then we have to alter the value of
+ # mlist.subscribe_policy as passed to RadioButtonArray in order to
+ # compensate for the fact that there is one fewer option.
+ # Correspondingly, we alter the value back in the change options
+ # function -scott
+ #
+ # TBD: this is an ugly ugly hack.
+ if varname.startswith('_'):
+ checked = 0
+ else:
+ checked = value
+ if varname == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
+ checked = checked - 1
+ # For Radio buttons, we're going to interpret the extra stuff as a
+ # horizontal/vertical flag. For backwards compatibility, the value 0
+ # means horizontal, so we use "not extra" to get the parity right.
+ return RadioButtonArray(varname, params, checked, not extra)
+ elif (kind == mm_cfg.String or kind == mm_cfg.Email or
+ kind == mm_cfg.Host or kind == mm_cfg.Number):
+ return TextBox(varname, value, params)
+ elif kind == mm_cfg.Text:
+ if params:
+ r, c = params
+ else:
+ r, c = None, None
+ return TextArea(varname, value or '', r, c)
+ elif kind in (mm_cfg.EmailList, mm_cfg.EmailListEx):
+ if params:
+ r, c = params
+ else:
+ r, c = None, None
+ res = NL.join(value)
+ return TextArea(varname, res, r, c, wrap='off')
+ elif kind == mm_cfg.FileUpload:
+ # like a text area, but also with uploading
+ if params:
+ r, c = params
+ else:
+ r, c = None, None
+ container = Container()
+ container.AddItem(_('<em>Enter the text below, or...</em><br>'))
+ container.AddItem(TextArea(varname, value or '', r, c))
+ container.AddItem(_('<br><em>...specify a file to upload</em><br>'))
+ container.AddItem(FileUpload(varname+'_upload', r, c))
+ return container
+ elif kind == mm_cfg.Select:
+ if params:
+ values, legend, selected = params
+ else:
+ values = mlist.GetAvailableLanguages()
+ legend = map(_, map(Utils.GetLanguageDescr, values))
+ selected = values.index(mlist.preferred_language)
+ return SelectOptions(varname, values, legend, selected)
+ elif kind == mm_cfg.Topics:
+ # A complex and specialized widget type that allows for setting of a
+ # topic name, a mark button, a regexp text box, an "add after mark",
+ # and a delete button. Yeesh! params are ignored.
+ table = Table(border=0)
+ # This adds the html for the entry widget
+ def makebox(i, name, pattern, desc, empty=0, table=table):
+ deltag = 'topic_delete_%02d' % i
+ boxtag = 'topic_box_%02d' % i
+ reboxtag = 'topic_rebox_%02d' % i
+ desctag = 'topic_desc_%02d' % i
+ wheretag = 'topic_where_%02d' % i
+ addtag = 'topic_add_%02d' % i
+ newtag = 'topic_new_%02d' % i
+ if empty:
+ table.AddRow([Center(Bold(_('Topic %(i)d'))),
+ Hidden(newtag)])
+ else:
+ table.AddRow([Center(Bold(_('Topic %(i)d'))),
+ SubmitButton(deltag, _('Delete'))])
+ table.AddRow([Label(_('Topic name:')),
+ TextBox(boxtag, value=name, size=30)])
+ table.AddRow([Label(_('Regexp:')),
+ TextArea(reboxtag, text=pattern,
+ rows=4, cols=30, wrap='off')])
+ table.AddRow([Label(_('Description:')),
+ TextArea(desctag, text=desc,
+ rows=4, cols=30, wrap='soft')])
+ if not empty:
+ table.AddRow([SubmitButton(addtag, _('Add new item...')),
+ SelectOptions(wheretag, ('before', 'after'),
+ (_('...before this one.'),
+ _('...after this one.')),
+ selected=1),
+ ])
+ table.AddRow(['<hr>'])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ # Now for each element in the existing data, create a widget
+ i = 1
+ data = getattr(mlist, varname)
+ for name, pattern, desc, empty in data:
+ makebox(i, name, pattern, desc, empty)
+ i += 1
+ # Add one more non-deleteable widget as the first blank entry, but
+ # only if there are no real entries.
+ if i == 1:
+ makebox(i, '', '', '', empty=1)
+ return table
+ elif kind == mm_cfg.Checkbox:
+ return CheckBoxArray(varname, *params)
+ else:
+ assert 0, 'Bad gui widget type: %s' % kind
+
+
+
+def get_item_gui_description(mlist, category, subcat,
+ varname, descr, elaboration, detailsp):
+ # Return the item's description, with link to details.
+ #
+ # Details are not included if this is a VARHELP page, because that /is/
+ # the details page!
+ if detailsp:
+ if subcat:
+ varhelp = '/?VARHELP=%s/%s/%s' % (category, subcat, varname)
+ else:
+ varhelp = '/?VARHELP=%s/%s' % (category, varname)
+ if descr == elaboration:
+ linktext = _('<br>(Edit <b>%(varname)s</b>)')
+ else:
+ linktext = _('<br>(Details for <b>%(varname)s</b>)')
+ link = Link(mlist.GetScriptURL('admin') + varhelp,
+ linktext).Format()
+ text = Label('%s %s' % (descr, link)).Format()
+ else:
+ text = Label(descr).Format()
+ if varname[0] == '_':
+ text += Label(_('''<br><em><strong>Note:</strong>
+ setting this value performs an immediate action but does not modify
+ permanent state.</em>''')).Format()
+ return text
+
+
+
+def membership_options(mlist, subcat, cgidata, doc, form):
+ # Show the main stuff
+ adminurl = mlist.GetScriptURL('admin', absolute=1)
+ container = Container()
+ header = Table(width="100%")
+ # If we're in the list subcategory, show the membership list
+ if subcat == 'add':
+ header.AddRow([Center(Header(2, _('Mass Subscriptions')))])
+ header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ container.AddItem(header)
+ mass_subscribe(mlist, container)
+ return container
+ if subcat == 'remove':
+ header.AddRow([Center(Header(2, _('Mass Removals')))])
+ header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ container.AddItem(header)
+ mass_remove(mlist, container)
+ return container
+ # Otherwise...
+ header.AddRow([Center(Header(2, _('Membership List')))])
+ header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ container.AddItem(header)
+ # Add a "search for member" button
+ table = Table(width='100%')
+ link = Link('http://www.python.org/doc/current/lib/re-syntax.html',
+ _('(help)')).Format()
+ table.AddRow([Label(_('Find member %(link)s:')),
+ TextBox('findmember',
+ value=cgidata.getvalue('findmember', '')),
+ SubmitButton('findmember_btn', _('Search...'))])
+ container.AddItem(table)
+ container.AddItem('<hr><p>')
+ usertable = Table(width="90%", border='2')
+ # If there are more members than allowed by chunksize, then we split the
+ # membership up alphabetically. Otherwise just display them all.
+ chunksz = mlist.admin_member_chunksize
+ all = mlist.getMembers()
+ all.sort(lambda x, y: cmp(x.lower(), y.lower()))
+ # See if the query has a regular expression
+ regexp = cgidata.getvalue('findmember', '').strip()
+ if regexp:
+ try:
+ cre = re.compile(regexp, re.IGNORECASE)
+ except re.error:
+ doc.addError(_('Bad regular expression: ') + regexp)
+ else:
+ # BAW: There's got to be a more efficient way of doing this!
+ names = [mlist.getMemberName(s) or '' for s in all]
+ all = [a for n, a in zip(names, all)
+ if cre.search(n) or cre.search(a)]
+ chunkindex = None
+ bucket = None
+ actionurl = None
+ if len(all) < chunksz:
+ members = all
+ else:
+ # Split them up alphabetically, and then split the alphabetical
+ # listing by chunks
+ buckets = {}
+ for addr in all:
+ members = buckets.setdefault(addr[0].lower(), [])
+ members.append(addr)
+ # Now figure out which bucket we want
+ bucket = None
+ qs = {}
+ # POST methods, even if their actions have a query string, don't get
+ # put into FieldStorage's keys :-(
+ qsenviron = os.environ.get('QUERY_STRING')
+ if qsenviron:
+ qs = cgi.parse_qs(qsenviron)
+ bucket = qs.get('letter', 'a')[0].lower()
+ if bucket not in digits + lowercase:
+ bucket = None
+ if not bucket or not buckets.has_key(bucket):
+ keys = buckets.keys()
+ keys.sort()
+ bucket = keys[0]
+ members = buckets[bucket]
+ action = adminurl + '/members?letter=%s' % bucket
+ if len(members) <= chunksz:
+ form.set_action(action)
+ else:
+ i, r = divmod(len(members), chunksz)
+ numchunks = i + (not not r * 1)
+ # Now chunk them up
+ chunkindex = 0
+ if qs.has_key('chunk'):
+ try:
+ chunkindex = int(qs['chunk'][0])
+ except ValueError:
+ chunkindex = 0
+ if chunkindex < 0 or chunkindex > numchunks:
+ chunkindex = 0
+ members = members[chunkindex*chunksz:(chunkindex+1)*chunksz]
+ # And set the action URL
+ form.set_action(action + '&chunk=%s' % chunkindex)
+ # So now members holds all the addresses we're going to display
+ allcnt = len(all)
+ if bucket:
+ membercnt = len(members)
+ usertable.AddRow([Center(Italic(_(
+ '%(allcnt)s members total, %(membercnt)s shown')))])
+ else:
+ usertable.AddRow([Center(Italic(_('%(allcnt)s members total')))])
+ usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
+ usertable.GetCurrentCellIndex(),
+ colspan=OPTCOLUMNS,
+ bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
+ # Add the alphabetical links
+ if bucket:
+ cells = []
+ for letter in digits + lowercase:
+ if not buckets.get(letter):
+ continue
+ url = adminurl + '/members?letter=%s' % letter
+ if letter == bucket:
+ show = Bold('[%s]' % letter.upper()).Format()
+ else:
+ show = letter.upper()
+ cells.append(Link(url, show).Format())
+ joiner = '&nbsp;'*2 + '\n'
+ usertable.AddRow([Center(joiner.join(cells))])
+ usertable.AddCellInfo(usertable.GetCurrentRowIndex(),
+ usertable.GetCurrentCellIndex(),
+ colspan=OPTCOLUMNS,
+ bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
+ usertable.AddRow([Center(h) for h in (_('unsub'),
+ _('member address<br>member name'),
+ _('mod'), _('hide'),
+ _('nomail<br>[reason]'),
+ _('ack'), _('not metoo'),
+ _('nodupes'),
+ _('digest'), _('plain'),
+ _('language'))])
+ rowindex = usertable.GetCurrentRowIndex()
+ for i in range(OPTCOLUMNS):
+ usertable.AddCellInfo(rowindex, i, bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
+ # Find the longest name in the list
+ longest = 0
+ if members:
+ names = filter(None, [mlist.getMemberName(s) for s in members])
+ # Make the name field at least as long as the longest email address
+ longest = max([len(s) for s in names + members])
+ # Abbreviations for delivery status details
+ ds_abbrevs = {MemberAdaptor.UNKNOWN : _('?'),
+ MemberAdaptor.BYUSER : _('U'),
+ MemberAdaptor.BYADMIN : _('A'),
+ MemberAdaptor.BYBOUNCE: _('B'),
+ }
+ # Now populate the rows
+ for addr in members:
+ link = Link(mlist.GetOptionsURL(addr, obscure=1),
+ mlist.getMemberCPAddress(addr))
+ fullname = Utils.uncanonstr(mlist.getMemberName(addr),
+ mlist.preferred_language)
+ name = TextBox(addr + '_realname', fullname, size=longest).Format()
+ cells = [Center(CheckBox(addr + '_unsub', 'off', 0).Format()),
+ link.Format() + '<br>' +
+ name +
+ Hidden('user', urllib.quote(addr)).Format(),
+ ]
+ # Do the `mod' option
+ if mlist.getMemberOption(addr, mm_cfg.Moderate):
+ value = 'on'
+ checked = 1
+ else:
+ value = 'off'
+ checked = 0
+ box = CheckBox('%s_mod' % addr, value, checked)
+ cells.append(Center(box).Format())
+ for opt in ('hide', 'nomail', 'ack', 'notmetoo', 'nodupes'):
+ extra = ''
+ if opt == 'nomail':
+ status = mlist.getDeliveryStatus(addr)
+ if status == MemberAdaptor.ENABLED:
+ value = 'off'
+ checked = 0
+ else:
+ value = 'on'
+ checked = 1
+ extra = '[%s]' % ds_abbrevs[status]
+ elif mlist.getMemberOption(addr, mm_cfg.OPTINFO[opt]):
+ value = 'on'
+ checked = 1
+ else:
+ value = 'off'
+ checked = 0
+ box = CheckBox('%s_%s' % (addr, opt), value, checked)
+ cells.append(Center(box.Format() + extra))
+ # This code is less efficient than the original which did a has_key on
+ # the underlying dictionary attribute. This version is slower and
+ # less memory efficient. It points to a new MemberAdaptor interface
+ # method.
+ if addr in mlist.getRegularMemberKeys():
+ cells.append(Center(CheckBox(addr + '_digest', 'off', 0).Format()))
+ else:
+ cells.append(Center(CheckBox(addr + '_digest', 'on', 1).Format()))
+ if mlist.getMemberOption(addr, mm_cfg.OPTINFO['plain']):
+ value = 'on'
+ checked = 1
+ else:
+ value = 'off'
+ checked = 0
+ cells.append(Center(CheckBox('%s_plain' % addr, value, checked)))
+ # User's preferred language
+ langpref = mlist.getMemberLanguage(addr)
+ langs = mlist.GetAvailableLanguages()
+ langdescs = [_(Utils.GetLanguageDescr(lang)) for lang in langs]
+ try:
+ selected = langs.index(langpref)
+ except ValueError:
+ selected = 0
+ cells.append(Center(SelectOptions(addr + '_language', langs,
+ langdescs, selected)).Format())
+ usertable.AddRow(cells)
+ # Add the usertable and a legend
+ legend = UnorderedList()
+ legend.AddItem(
+ _('<b>unsub</b> -- Click on this to unsubscribe the member.'))
+ legend.AddItem(
+ _("""<b>mod</b> -- The user's personal moderation flag. If this is
+ set, postings from them will be moderated, otherwise they will be
+ approved."""))
+ legend.AddItem(
+ _("""<b>hide</b> -- Is the member's address concealed on
+ the list of subscribers?"""))
+ legend.AddItem(_(
+ """<b>nomail</b> -- Is delivery to the member disabled? If so, an
+ abbreviation will be given describing the reason for the disabled
+ delivery:
+ <ul><li><b>U</b> -- Delivery was disabled by the user via their
+ personal options page.
+ <li><b>A</b> -- Delivery was disabled by the list
+ administrators.
+ <li><b>B</b> -- Delivery was disabled by the system due to
+ excessive bouncing from the member's address.
+ <li><b>?</b> -- The reason for disabled delivery isn't known.
+ This is the case for all memberships which were disabled
+ in older versions of Mailman.
+ </ul>"""))
+ legend.AddItem(
+ _('''<b>ack</b> -- Does the member get acknowledgements of their
+ posts?'''))
+ legend.AddItem(
+ _('''<b>not metoo</b> -- Does the member want to avoid copies of their
+ own postings?'''))
+ legend.AddItem(
+ _('''<b>nodupes</b> -- Does the member want to avoid duplicates of the
+ same message?'''))
+ legend.AddItem(
+ _('''<b>digest</b> -- Does the member get messages in digests?
+ (otherwise, individual messages)'''))
+ legend.AddItem(
+ _('''<b>plain</b> -- If getting digests, does the member get plain
+ text digests? (otherwise, MIME)'''))
+ legend.AddItem(_("<b>language</b> -- Language preferred by the user"))
+ addlegend = ''
+ parsedqs = 0
+ qsenviron = os.environ.get('QUERY_STRING')
+ if qsenviron:
+ qs = cgi.parse_qs(qsenviron).get('legend')
+ if qs and isinstance(qs, ListType):
+ qs = qs[0]
+ if qs == 'yes':
+ addlegend = 'legend=yes&'
+ if addlegend:
+ container.AddItem(legend.Format() + '<p>')
+ container.AddItem(
+ Link(adminurl + '/members/list',
+ _('Click here to hide the legend for this table.')))
+ else:
+ container.AddItem(
+ Link(adminurl + '/members/list?legend=yes',
+ _('Click here to include the legend for this table.')))
+ container.AddItem(Center(usertable))
+
+ # There may be additional chunks
+ if chunkindex is not None:
+ buttons = []
+ url = adminurl + '/members?%sletter=%s&' % (addlegend, bucket)
+ footer = _('''<p><em>To view more members, click on the appropriate
+ range listed below:</em>''')
+ chunkmembers = buckets[bucket]
+ last = len(chunkmembers)
+ for i in range(numchunks):
+ if i == chunkindex:
+ continue
+ start = chunkmembers[i*chunksz]
+ end = chunkmembers[min((i+1)*chunksz, last)-1]
+ link = Link(url + 'chunk=%d' % i, _('from %(start)s to %(end)s'))
+ buttons.append(link)
+ buttons = UnorderedList(*buttons)
+ container.AddItem(footer + buttons.Format() + '<p>')
+ return container
+
+
+
+def mass_subscribe(mlist, container):
+ # MASS SUBSCRIBE
+ GREY = mm_cfg.WEB_ADMINITEM_COLOR
+ table = Table(width='90%')
+ table.AddRow([
+ Label(_('Subscribe these users now or invite them?')),
+ RadioButtonArray('subscribe_or_invite',
+ (_('Subscribe'), _('Invite')),
+ 0, values=(0, 1))
+ ])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
+ table.AddRow([
+ Label(_('Send welcome messages to new subscribees?')),
+ RadioButtonArray('send_welcome_msg_to_this_batch',
+ (_('No'), _('Yes')),
+ mlist.send_welcome_msg,
+ values=(0, 1))
+ ])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
+ table.AddRow([
+ Label(_('Send notifications of new subscriptions to the list owner?')),
+ RadioButtonArray('send_notifications_to_list_owner',
+ (_('No'), _('Yes')),
+ mlist.admin_notify_mchanges,
+ values=(0,1))
+ ])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
+ table.AddRow([Italic(_('Enter one address per line below...'))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Center(TextArea(name='subscribees',
+ rows=10, cols='70%', wrap=None))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
+ FileUpload('subscribees_upload', cols='50')])
+ container.AddItem(Center(table))
+ # Invitation text
+ table.AddRow(['&nbsp;', '&nbsp;'])
+ table.AddRow([Italic(_("""Below, enter additional text to be added to the
+ top of your invitation or the subscription notification. Include at least
+ one blank line at the end..."""))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Center(TextArea(name='invitation',
+ rows=10, cols='70%', wrap=None))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+
+
+
+def mass_remove(mlist, container):
+ # MASS UNSUBSCRIBE
+ GREY = mm_cfg.WEB_ADMINITEM_COLOR
+ table = Table(width='90%')
+ table.AddRow([
+ Label(_('Send unsubscription acknowledgement to the user?')),
+ RadioButtonArray('send_unsub_ack_to_this_batch',
+ (_('No'), _('Yes')),
+ 0, values=(0, 1))
+ ])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
+ table.AddRow([
+ Label(_('Send notifications to the list owner?')),
+ RadioButtonArray('send_unsub_notifications_to_list_owner',
+ (_('No'), _('Yes')),
+ mlist.admin_notify_mchanges,
+ values=(0, 1))
+ ])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY)
+ table.AddRow([Italic(_('Enter one address per line below...'))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Center(TextArea(name='unsubscribees',
+ rows=10, cols='70%', wrap=None))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Italic(Label(_('...or specify a file to upload:'))),
+ FileUpload('unsubscribees_upload', cols='50')])
+ container.AddItem(Center(table))
+
+
+
+def password_inputs(mlist):
+ adminurl = mlist.GetScriptURL('admin', absolute=1)
+ table = Table(cellspacing=3, cellpadding=4)
+ table.AddRow([Center(Header(2, _('Change list ownership passwords')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ table.AddRow([_("""\
+The <em>list administrators</em> are the people who have ultimate control over
+all parameters of this mailing list. They are able to change any list
+configuration variable available through these administration web pages.
+
+<p>The <em>list moderators</em> have more limited permissions; they are not
+able to change any list configuration variable, but they are allowed to tend
+to pending administration requests, including approving or rejecting held
+subscription requests, and disposing of held postings. Of course, the
+<em>list administrators</em> can also tend to pending requests.
+
+<p>In order to split the list ownership duties into administrators and
+moderators, you must set a separate moderator password in the fields below,
+and also provide the email addresses of the list moderators in the
+<a href="%(adminurl)s/general">general options section</a>.""")])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ # Set up the admin password table on the left
+ atable = Table(border=0, cellspacing=3, cellpadding=4,
+ bgcolor=mm_cfg.WEB_ADMINPW_COLOR)
+ atable.AddRow([Label(_('Enter new administrator password:')),
+ PasswordBox('newpw', size=20)])
+ atable.AddRow([Label(_('Confirm administrator password:')),
+ PasswordBox('confirmpw', size=20)])
+ # Set up the moderator password table on the right
+ mtable = Table(border=0, cellspacing=3, cellpadding=4,
+ bgcolor=mm_cfg.WEB_ADMINPW_COLOR)
+ mtable.AddRow([Label(_('Enter new moderator password:')),
+ PasswordBox('newmodpw', size=20)])
+ mtable.AddRow([Label(_('Confirm moderator password:')),
+ PasswordBox('confirmmodpw', size=20)])
+ # Add these tables to the overall password table
+ table.AddRow([atable, mtable])
+ return table
+
+
+
+def submit_button(name='submit'):
+ table = Table(border=0, cellspacing=0, cellpadding=2)
+ table.AddRow([Bold(SubmitButton(name, _('Submit Your Changes')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, align='middle')
+ return table
+
+
+
+def change_options(mlist, category, subcat, cgidata, doc):
+ def safeint(formvar, defaultval=None):
+ try:
+ return int(cgidata.getvalue(formvar))
+ except (ValueError, TypeError):
+ return defaultval
+ confirmed = 0
+ # Handle changes to the list moderator password. Do this before checking
+ # the new admin password, since the latter will force a reauthentication.
+ new = cgidata.getvalue('newmodpw', '').strip()
+ confirm = cgidata.getvalue('confirmmodpw', '').strip()
+ if new or confirm:
+ if new == confirm:
+ mlist.mod_password = sha.new(new).hexdigest()
+ # No re-authentication necessary because the moderator's
+ # password doesn't get you into these pages.
+ else:
+ doc.addError(_('Moderator passwords did not match'))
+ # Handle changes to the list administrator password
+ new = cgidata.getvalue('newpw', '').strip()
+ confirm = cgidata.getvalue('confirmpw', '').strip()
+ if new or confirm:
+ if new == confirm:
+ mlist.password = sha.new(new).hexdigest()
+ # Set new cookie
+ print mlist.MakeCookie(mm_cfg.AuthListAdmin)
+ else:
+ doc.addError(_('Administrator passwords did not match'))
+ # Give the individual gui item a chance to process the form data
+ categories = mlist.GetConfigCategories()
+ label, gui = categories[category]
+ # BAW: We handle the membership page special... for now.
+ if category <> 'members':
+ gui.handleForm(mlist, category, subcat, cgidata, doc)
+ # mass subscription, removal processing for members category
+ subscribers = ''
+ subscribers += cgidata.getvalue('subscribees', '')
+ subscribers += cgidata.getvalue('subscribees_upload', '')
+ if subscribers:
+ entries = filter(None, [n.strip() for n in subscribers.splitlines()])
+ send_welcome_msg = safeint('send_welcome_msg_to_this_batch',
+ mlist.send_welcome_msg)
+ send_admin_notif = safeint('send_notifications_to_list_owner',
+ mlist.admin_notify_mchanges)
+ # Default is to subscribe
+ subscribe_or_invite = safeint('subscribe_or_invite', 0)
+ invitation = cgidata.getvalue('invitation', '')
+ digest = 0
+ if not mlist.digestable:
+ digest = 0
+ if not mlist.nondigestable:
+ digest = 1
+ subscribe_errors = []
+ subscribe_success = []
+ # Now cruise through all the subscribees and do the deed. BAW: we
+ # should limit the number of "Successfully subscribed" status messages
+ # we display. Try uploading a file with 10k names -- it takes a while
+ # to render the status page.
+ for entry in entries:
+ fullname, address = parseaddr(entry)
+ # Canonicalize the full name
+ fullname = Utils.canonstr(fullname, mlist.preferred_language)
+ userdesc = UserDesc(address, fullname,
+ Utils.MakeRandomPassword(),
+ digest, mlist.preferred_language)
+ try:
+ if subscribe_or_invite:
+ if mlist.isMember(address):
+ raise Errors.MMAlreadyAMember
+ else:
+ mlist.InviteNewMember(userdesc, invitation)
+ else:
+ mlist.ApprovedAddMember(userdesc, send_welcome_msg,
+ send_admin_notif, invitation)
+ except Errors.MMAlreadyAMember:
+ subscribe_errors.append((entry, _('Already a member')))
+ except Errors.MMBadEmailError:
+ if userdesc.address == '':
+ subscribe_errors.append((_('&lt;blank line&gt;'),
+ _('Bad/Invalid email address')))
+ else:
+ subscribe_errors.append((entry,
+ _('Bad/Invalid email address')))
+ except Errors.MMHostileAddress:
+ subscribe_errors.append(
+ (entry, _('Hostile address (illegal characters)')))
+ else:
+ member = Utils.uncanonstr(formataddr((fullname, address)))
+ subscribe_success.append(Utils.websafe(member))
+ if subscribe_success:
+ if subscribe_or_invite:
+ doc.AddItem(Header(5, _('Successfully invited:')))
+ else:
+ doc.AddItem(Header(5, _('Successfully subscribed:')))
+ doc.AddItem(UnorderedList(*subscribe_success))
+ doc.AddItem('<p>')
+ if subscribe_errors:
+ if subscribe_or_invite:
+ doc.AddItem(Header(5, _('Error inviting:')))
+ else:
+ doc.AddItem(Header(5, _('Error subscribing:')))
+ items = ['%s -- %s' % (x0, x1) for x0, x1 in subscribe_errors]
+ doc.AddItem(UnorderedList(*items))
+ doc.AddItem('<p>')
+ # Unsubscriptions
+ removals = ''
+ if cgidata.has_key('unsubscribees'):
+ removals += cgidata['unsubscribees'].value
+ if cgidata.has_key('unsubscribees_upload') and \
+ cgidata['unsubscribees_upload'].value:
+ removals += cgidata['unsubscribees_upload'].value
+ if removals:
+ names = filter(None, [n.strip() for n in removals.splitlines()])
+ send_unsub_notifications = int(
+ cgidata['send_unsub_notifications_to_list_owner'].value)
+ userack = int(
+ cgidata['send_unsub_ack_to_this_batch'].value)
+ unsubscribe_errors = []
+ unsubscribe_success = []
+ for addr in names:
+ try:
+ mlist.ApprovedDeleteMember(
+ addr, whence='admin mass unsub',
+ admin_notif=send_unsub_notifications,
+ userack=userack)
+ unsubscribe_success.append(addr)
+ except Errors.NotAMemberError:
+ unsubscribe_errors.append(addr)
+ if unsubscribe_success:
+ doc.AddItem(Header(5, _('Successfully Unsubscribed:')))
+ doc.AddItem(UnorderedList(*unsubscribe_success))
+ doc.AddItem('<p>')
+ if unsubscribe_errors:
+ doc.AddItem(Header(3, Bold(FontAttr(
+ _('Cannot unsubscribe non-members:'),
+ color='#ff0000', size='+2')).Format()))
+ doc.AddItem(UnorderedList(*unsubscribe_errors))
+ doc.AddItem('<p>')
+ # See if this was a moderation bit operation
+ if cgidata.has_key('allmodbit_btn'):
+ val = cgidata.getvalue('allmodbit_val')
+ try:
+ val = int(val)
+ except VallueError:
+ val = None
+ if val not in (0, 1):
+ doc.addError(_('Bad moderation flag value'))
+ else:
+ for member in mlist.getMembers():
+ mlist.setMemberOption(member, mm_cfg.Moderate, val)
+ # do the user options for members category
+ if cgidata.has_key('setmemberopts_btn') and cgidata.has_key('user'):
+ user = cgidata['user']
+ if type(user) is ListType:
+ users = []
+ for ui in range(len(user)):
+ users.append(urllib.unquote(user[ui].value))
+ else:
+ users = [urllib.unquote(user.value)]
+ errors = []
+ removes = []
+ for user in users:
+ if cgidata.has_key('%s_unsub' % user):
+ try:
+ mlist.ApprovedDeleteMember(user)
+ removes.append(user)
+ except Errors.NotAMemberError:
+ errors.append((user, _('Not subscribed')))
+ continue
+ if not mlist.isMember(user):
+ doc.addError(_('Ignoring changes to deleted member: %(user)s'),
+ tag=_('Warning: '))
+ continue
+ value = cgidata.has_key('%s_digest' % user)
+ try:
+ mlist.setMemberOption(user, mm_cfg.Digests, value)
+ except (Errors.AlreadyReceivingDigests,
+ Errors.AlreadyReceivingRegularDeliveries,
+ Errors.CantDigestError,
+ Errors.MustDigestError):
+ # BAW: Hmm...
+ pass
+
+ newname = cgidata.getvalue(user+'_realname', '')
+ newname = Utils.canonstr(newname, mlist.preferred_language)
+ mlist.setMemberName(user, newname)
+
+ newlang = cgidata.getvalue(user+'_language')
+ oldlang = mlist.getMemberLanguage(user)
+ if newlang and newlang <> oldlang:
+ mlist.setMemberLanguage(user, newlang)
+
+ moderate = not not cgidata.getvalue(user+'_mod')
+ mlist.setMemberOption(user, mm_cfg.Moderate, moderate)
+
+ # Set the `nomail' flag, but only if the user isn't already
+ # disabled (otherwise we might change BYUSER into BYADMIN).
+ if cgidata.has_key('%s_nomail' % user):
+ if mlist.getDeliveryStatus(user) == MemberAdaptor.ENABLED:
+ mlist.setDeliveryStatus(user, MemberAdaptor.BYADMIN)
+ else:
+ mlist.setDeliveryStatus(user, MemberAdaptor.ENABLED)
+ for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain'):
+ opt_code = mm_cfg.OPTINFO[opt]
+ if cgidata.has_key('%s_%s' % (user, opt)):
+ mlist.setMemberOption(user, opt_code, 1)
+ else:
+ mlist.setMemberOption(user, opt_code, 0)
+ # Give some feedback on who's been removed
+ if removes:
+ doc.AddItem(Header(5, _('Successfully Removed:')))
+ doc.AddItem(UnorderedList(*removes))
+ doc.AddItem('<p>')
+ if errors:
+ doc.AddItem(Header(5, _("Error Unsubscribing:")))
+ items = ['%s -- %s' % (x[0], x[1]) for x in errors]
+ doc.AddItem(apply(UnorderedList, tuple((items))))
+ doc.AddItem("<p>")
diff --git a/Mailman/Cgi/admindb.py b/Mailman/Cgi/admindb.py
new file mode 100644
index 00000000..e6b71cda
--- /dev/null
+++ b/Mailman/Cgi/admindb.py
@@ -0,0 +1,769 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""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 readMessage
+from Mailman.Cgi import Auth
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+EMPTYSTRING = ''
+NL = '\n'
+
+# Set up i18n. Until we know which list is being requested, we use the
+# server's default.
+_ = 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):
+ # We can't use a RadioButtonArray here because horizontal placement can be
+ # confusing to the user and vertical placement takes up too much
+ # real-estate. This is a hack!
+ space = '&nbsp;' * 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():
+ # Figure out which list is being requested
+ 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:
+ # Avoid cross-site scripting attacks
+ 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
+
+ # Now that we know which list to use, set the system's language to it.
+ i18n.set_language(mlist.preferred_language)
+
+ # Make sure the user is authorized to see this page.
+ 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'):
+ # This is a re-authorization attempt
+ msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
+ else:
+ msg = ''
+ Auth.loginpage(mlist, 'admindb', msg=msg)
+ return
+
+ # Set up the results document
+ doc = Document()
+ doc.set_language(mlist.preferred_language)
+
+ # See if we're requesting all the messages for a particular sender, or if
+ # we want a specific held message.
+ sender = None
+ msgid = None
+ details = None
+ envar = os.environ.get('QUERY_STRING')
+ if envar:
+ # POST methods, even if their actions have a query string, don't get
+ # put into FieldStorage's keys :-(
+ 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]
+
+ # We need a signal handler to catch the SIGTERM that can come from Apache
+ # when the user hits the browser's STOP button. See the comment in
+ # admin.py for details.
+ #
+ # BAW: Strictly speaking, the list should not need to be locked just to
+ # read the request database. However the request database asserts that
+ # the list is locked in order to load it and it's not worth complicating
+ # that logic.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ # Make sure the list gets unlocked...
+ mlist.Unlock()
+ # ...and ensure we exit, otherwise race conditions could cause us to
+ # enter MailList.Save() while we're in the unlocked state, and that
+ # could be bad!
+ sys.exit(0)
+
+ mlist.Lock()
+ try:
+ # Install the emergency shutdown signal handler
+ signal.signal(signal.SIGTERM, sigterm_handler)
+
+ realname = mlist.real_name
+ if not cgidata.keys():
+ # If this is not a form submission (i.e. there are no keys in the
+ # form), then all we don't need to do much special.
+ doc.SetTitle(_('%(realname)s Administrative Database'))
+ elif not details:
+ # This is a form submission
+ doc.SetTitle(_('%(realname)s Administrative Database Results'))
+ process_form(mlist, doc, cgidata)
+ # Now print the results and we're done. Short circuit for when there
+ # are no pending requests, but be sure to save the results!
+ 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)
+ # Add the instructions template
+ if details:
+ 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 not details:
+ form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
+ # Add a link back to the overview, if we're not viewing the overview!
+ 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:
+ # Show a summary of all requests
+ 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
+ # Finish up the document, adding buttons to the form
+ if addform:
+ doc.AddItem(form)
+ form.AddItem('<hr>')
+ form.AddItem(Center(SubmitButton('submit', _('Submit All Data'))))
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ # Commit all changes
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def handle_no_list(msg=''):
+ # Print something useful if no list was given.
+ 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):
+ # Add the subscription request section
+ 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')))
+ ])
+ # Alphabetical order by email address
+ 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():
+ # Eliminate duplicates
+ 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() + \
+ '&nbsp;' + _('Permanently ban from this list')
+ table.AddRow(['%s<br><em>%s</em>' % (addr, fullname),
+ radio,
+ TextBox('comment-%d' % id, size=40)
+ ])
+ num += 1
+ if num > 0:
+ form.AddItem(table)
+ return num
+
+
+
+def show_pending_unsubs(mlist, form):
+ # Add the pending unsubscription request section
+ 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')))
+ ])
+ # Alphabetical order by email address
+ byaddrs = {}
+ for id in pendingunsubs:
+ addr = mlist.GetRecord(id)[1]
+ byaddrs.setdefault(addr, []).append(id)
+ addrs = byaddrs.keys()
+ addrs.sort()
+ num = 0
+ for addr, ids in byaddrs.items():
+ # Eliminate duplicates
+ 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:
+ # They must have been unsubscribed elsewhere, so we can just
+ # discard this record.
+ mlist.HandleRequest(id, mm_cfg.DISCARD)
+ continue
+ num += 1
+ table.AddRow(['%s<br><em>%s</em>' % (addr, 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):
+ # Sort the held messages by sender
+ bysender = helds_by_sender(mlist)
+ if not bysender:
+ return 0
+ # Add the by-sender overview tables
+ 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
+ # The encompassing sender table
+ 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() +
+ '&nbsp;' +
+ _('Preserve messages for the site administrator')
+ ])
+ left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2)
+ left.AddRow([
+ CheckBox('senderforward-' + qsender, 1).Format() +
+ '&nbsp;' +
+ _('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 the sender is a member and the message is being held due to a
+ # moderation bit, give the admin a chance to clear the member's mod
+ # bit. If this sender is not a member and is not already on one of
+ # the sender filters, then give the admin a chance to add this sender
+ # to one of the filters.
+ if mlist.isMember(sender):
+ if mlist.getMemberOption(sender, mm_cfg.Moderate):
+ left.AddRow([
+ CheckBox('senderclearmodp-' + qsender, 1).Format() +
+ '&nbsp;' +
+ _("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() +
+ '&nbsp;' +
+ _('Add <b>%(esender)s</b> to a sender filter')
+ ])
+ 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() +
+ '&nbsp;' +
+ _("""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(['&nbsp;', '&nbsp;'])
+ counter = 1
+ for id in bysender[sender]:
+ info = mlist.GetRecord(id)
+ ptime, sender, subject, reason, filename, msgdata = info
+ # BAW: This is really the size of the message pickle, which should
+ # be close, but won't be exact. Sigh, good enough.
+ try:
+ size = os.path.getsize(os.path.join(mm_cfg.DATA_DIR, filename))
+ except OSError, e:
+ if e.errno <> errno.ENOENT: raise
+ # This message must have gotten lost, i.e. it's already been
+ # handled by the time we got here.
+ mlist.HandleRequest(id, mm_cfg.DISCARD)
+ continue
+ t = Table(border=0)
+ t.AddRow([Link(admindburl + '?msgid=%d' % id, '[%d]' % counter),
+ Bold(_('Subject:')),
+ Utils.websafe(subject)
+ ])
+ t.AddRow(['&nbsp;', Bold(_('Size:')), str(size) + _(' bytes')])
+ if reason:
+ reason = _(reason)
+ else:
+ reason = _('not available')
+ t.AddRow(['&nbsp;', Bold(_('Reason:')), reason])
+ # Include the date we received the message, if available
+ when = msgdata.get('received_time')
+ if when:
+ t.AddRow(['&nbsp;', 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:
+ # BAW: should we print an error message?
+ 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):
+ # BAW: print an error message?
+ 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):
+ # For backwards compatibility with pre 2.0beta3
+ if len(info) == 5:
+ ptime, sender, subject, reason, filename = info
+ msgdata = {}
+ else:
+ ptime, sender, subject, reason, filename, msgdata = info
+ form.AddItem('<hr>')
+ # Header shown on each held posting (including count of total)
+ msg = _('Posting Held for Approval')
+ if total <> 1:
+ msg += _(' (%(count)d of %(total)d)')
+ form.AddItem(Center(Header(2, msg)))
+ # We need to get the headers and part of the textual body of the message
+ # being held. The best way to do this is to use the email Parser to get
+ # an actual object, which will be easier to deal with. We probably could
+ # just do raw reads on the file.
+ 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>')
+ # BAW: kludge to remove id from requests.db.
+ 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.'))
+ # BAW: Should we really delete this, or shuttle it off for site admin
+ # to look more closely at?
+ form.AddItem('<p>')
+ # BAW: kludge to remove id from requests.db.
+ try:
+ mlist.HandleRequest(id, mm_cfg.DISCARD)
+ except Errors.LostHeldMessage:
+ pass
+ return
+ # Get the header text and the message body excerpt
+ lines = []
+ chars = 0
+ # A negative value means, include the entire message regardless of size
+ 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
+ # Negative values mean display the entire message, regardless of size
+ if limit > 0:
+ body = EMPTYSTRING.join(lines)[:mm_cfg.ADMINDB_PAGE_TEXT_LIMIT]
+ else:
+ body = EMPTYSTRING.join(lines)
+ hdrtxt = NL.join(['%s: %s' % (k, v) for k, v in msg.items()])
+ hdrtxt = Utils.websafe(hdrtxt)
+ # Okay, we've reconstituted the message just fine. Now for the fun part!
+ 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(subject)])
+ 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')
+ # We can't use a RadioButtonArray here because horizontal placement can be
+ # confusing to the user and vertical placement takes up too much
+ # real-estate. This is a hack!
+ buttons = Table(cellspacing="5", cellpadding="0")
+ buttons.AddRow(map(lambda x, s='&nbsp;'*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(['&nbsp;',
+ CheckBox('preserve-%d' % id, 'on', 0).Format() +
+ '&nbsp;' + _('Preserve message for site administrator')
+ ])
+ t.AddRow(['&nbsp;',
+ CheckBox('forward-%d' % id, 'on', 0).Format() +
+ '&nbsp;' + _('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 = {}
+ # Sender-centric actions
+ 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
+ for sender in senderactions.keys():
+ actions = senderactions[sender]
+ # Handle what to do about all this sender's held messages
+ try:
+ action = int(actions.get('senderaction', mm_cfg.DEFER))
+ except ValueError:
+ action = mm_cfg.DEFER
+ 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', '')
+ comment = _('No reason given')
+ bysender = helds_by_sender(mlist)
+ for id in bysender.get(sender, []):
+ try:
+ mlist.HandleRequest(id, action, comment, preserve,
+ forward, forwardaddr)
+ except (KeyError, Errors.LostHeldMessage):
+ # That's okay, it just means someone else has already
+ # updated the database while we were staring at the page,
+ # so just ignore it
+ continue
+ # Now see if this sender should be added to one of the nonmember
+ # sender filters.
+ if actions.get('senderfilterp', 0):
+ try:
+ which = int(actions.get('senderfilter'))
+ except ValueError:
+ # Bogus form
+ 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)
+ # Otherwise, it's a bogus form, so ignore it
+ # And now see if we're to clear the member's moderation flag.
+ if actions.get('senderclearmodp', 0):
+ try:
+ mlist.setMemberOption(sender, mm_cfg.Moderate, 0)
+ except Errors.NotAMemberError:
+ # This person's not a member any more. Oh well.
+ pass
+ # And should this address be banned?
+ if actions.get('senderbanp', 0):
+ if sender not in mlist.ban_list:
+ mlist.ban_list.append(sender)
+ # Now, do message specific actions
+ 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
+ # Get the action comment and reasons if present.
+ 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
+ # Defaults
+ comment = _('[No reason 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
+ # Should we ban this address? Do this check before handling the
+ # request id because that will evict the record.
+ if cgidata.getvalue(bankey):
+ sender = mlist.GetRecord(request_id)[1]
+ if sender not in mlist.ban_list:
+ mlist.ban_list.append(sender)
+ # Handle the request id
+ try:
+ mlist.HandleRequest(request_id, v, comment,
+ preserve, forward, forwardaddr)
+ except (KeyError, Errors.LostHeldMessage):
+ # That's okay, it just means someone else has already updated the
+ # database while we were staring at the page, so just ignore it
+ continue
+ except Errors.MMAlreadyAMember, v:
+ erroraddrs.append(v)
+ # save the list and print the results
+ doc.AddItem(Header(2, _('Database Updated...')))
+ if erroraddrs:
+ for addr in erroraddrs:
+ doc.AddItem(`addr` + _(' is already a member') + '<br>')
diff --git a/Mailman/Cgi/confirm.py b/Mailman/Cgi/confirm.py
new file mode 100644
index 00000000..2348b0b6
--- /dev/null
+++ b/Mailman/Cgi/confirm.py
@@ -0,0 +1,791 @@
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Confirm a pending action via URL."""
+
+import signal
+import cgi
+import time
+
+from Mailman import mm_cfg
+from Mailman import Errors
+from Mailman import i18n
+from Mailman import MailList
+from Mailman import Pending
+from Mailman.UserDesc import UserDesc
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def main():
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ parts = Utils.GetPathPieces()
+ if not parts or len(parts) < 1:
+ bad_confirmation(doc)
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+ return
+
+ listname = parts[0].lower()
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ bad_confirmation(doc, _('No such list <em>%(safelistname)s</em>'))
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+ syslog('error', 'No such list "%s": %s', listname, e)
+ return
+
+ # Set the language for the list
+ i18n.set_language(mlist.preferred_language)
+ doc.set_language(mlist.preferred_language)
+
+ # Get the form data to see if this is a second-step confirmation
+ cgidata = cgi.FieldStorage(keep_blank_values=1)
+ cookie = cgidata.getvalue('cookie')
+ if cookie == '':
+ ask_for_cookie(mlist, doc, _('Confirmation string was empty.'))
+ return
+
+ if not cookie and len(parts) == 2:
+ cookie = parts[1]
+
+ if len(parts) > 2:
+ bad_confirmation(doc)
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ return
+
+ if not cookie:
+ ask_for_cookie(mlist, doc)
+ return
+
+ days = int(mm_cfg.PENDING_REQUEST_LIFE / mm_cfg.days(1) + 0.5)
+ confirmurl = mlist.GetScriptURL('confirm', absolute=1)
+ # Avoid cross-site scripting attacks
+ safecookie = Utils.websafe(cookie)
+ badconfirmstr = _('''<b>Invalid confirmation string:</b>
+ %(safecookie)s.
+
+ <p>Note that confirmation strings expire approximately
+ %(days)s days after the initial subscription request. If your
+ confirmation has expired, please try to re-submit your subscription.
+ Otherwise, <a href="%(confirmurl)s">re-enter</a> your confirmation
+ string.''')
+
+ content = Pending.confirm(cookie, expunge=0)
+ if content is None:
+ bad_confirmation(doc, badconfirmstr)
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ return
+
+ try:
+ if content[0] == Pending.SUBSCRIPTION:
+ if cgidata.getvalue('cancel'):
+ subscription_cancel(mlist, doc, cookie)
+ elif cgidata.getvalue('submit'):
+ subscription_confirm(mlist, doc, cookie, cgidata)
+ else:
+ subscription_prompt(mlist, doc, cookie, content[1])
+ elif content[0] == Pending.UNSUBSCRIPTION:
+ try:
+ if cgidata.getvalue('cancel'):
+ unsubscription_cancel(mlist, doc, cookie)
+ elif cgidata.getvalue('submit'):
+ unsubscription_confirm(mlist, doc, cookie)
+ else:
+ unsubscription_prompt(mlist, doc, cookie, *content[1:])
+ except Errors.NotAMemberError:
+ doc.addError(_("""The address requesting unsubscription is not
+ a member of the mailing list. Perhaps you have already been
+ unsubscribed, e.g. by the list administrator?"""))
+ # And get rid of this confirmation cookie
+ Pending.confirm(cookie)
+ elif content[0] == Pending.CHANGE_OF_ADDRESS:
+ if cgidata.getvalue('cancel'):
+ addrchange_cancel(mlist, doc, cookie)
+ elif cgidata.getvalue('submit'):
+ addrchange_confirm(mlist, doc, cookie)
+ else:
+ # Watch out for users who have unsubscribed themselves in the
+ # meantime!
+ try:
+ addrchange_prompt(mlist, doc, cookie, *content[1:])
+ except Errors.NotAMemberError:
+ doc.addError(_("""The address requesting to be changed has
+ been subsequently unsubscribed. This request has been
+ cancelled."""))
+ Pending.confirm(cookie, expunge=1)
+ elif content[0] == Pending.HELD_MESSAGE:
+ if cgidata.getvalue('cancel'):
+ heldmsg_cancel(mlist, doc, cookie)
+ elif cgidata.getvalue('submit'):
+ heldmsg_confirm(mlist, doc, cookie)
+ else:
+ heldmsg_prompt(mlist, doc, cookie, *content[1:])
+ elif content[0] == Pending.RE_ENABLE:
+ if cgidata.getvalue('cancel'):
+ reenable_cancel(mlist, doc, cookie)
+ elif cgidata.getvalue('submit'):
+ reenable_confirm(mlist, doc, cookie)
+ else:
+ reenable_prompt(mlist, doc, cookie, *content[1:])
+ else:
+ bad_confirmation(doc, _('System error, bad content: %(content)s'))
+ except Errors.MMBadConfirmation:
+ bad_confirmation(doc, badconfirmstr)
+
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+
+
+
+def bad_confirmation(doc, extra=''):
+ title = _('Bad confirmation string')
+ doc.SetTitle(title)
+ doc.AddItem(Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
+ doc.AddItem(extra)
+
+
+
+def ask_for_cookie(mlist, doc, extra=''):
+ title = _('Enter confirmation cookie')
+ doc.SetTitle(title)
+ form = Form(mlist.GetScriptURL('confirm', 1))
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ colspan=2, bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ if extra:
+ table.AddRow([Bold(FontAttr(extra, size='+1'))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+
+ # Add cookie entry box
+ table.AddRow([_("""Please enter the confirmation string
+ (i.e. <em>cookie</em>) that you received in your email message, in the box
+ below. Then hit the <em>Submit</em> button to proceed to the next
+ confirmation step.""")])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Label(_('Confirmation string:')),
+ TextBox('cookie')])
+ table.AddRow([Center(SubmitButton('submit_cookie', _('Submit')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ form.AddItem(table)
+ doc.AddItem(form)
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+
+
+
+def subscription_prompt(mlist, doc, cookie, userdesc):
+ email = userdesc.address
+ password = userdesc.password
+ digest = userdesc.digest
+ lang = userdesc.language
+ name = Utils.uncanonstr(userdesc.fullname, lang)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ title = _('Confirm subscription request')
+ doc.SetTitle(title)
+
+ form = Form(mlist.GetScriptURL('confirm', 1))
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ colspan=2, bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ listname = mlist.real_name
+ # This is the normal, no-confirmation required results text.
+ #
+ # We do things this way so we don't have to reformat this paragraph, which
+ # would mess up translations. If you modify this text for other reasons,
+ # please refill the paragraph, and clean up the logic.
+ result = _("""Your confirmation is required in order to complete the
+ subscription request to the mailing list <em>%(listname)s</em>. Your
+ subscription settings are shown below; make any necessary changes and hit
+ <em>Subscribe</em> to complete the confirmation process. Once you've
+ confirmed your subscription request, you will be shown your account
+ options page which you can use to further customize your membership
+ options.
+
+ <p>Note: your password will be emailed to you once your subscription is
+ confirmed. You can change it by visiting your personal options page.
+
+ <p>Or hit <em>Cancel and discard</em> to cancel this subscription
+ request.""") + '<p><hr>'
+ if mlist.subscribe_policy in (2, 3):
+ # Confirmation is required
+ result = _("""Your confirmation is required in order to continue with
+ the subscription request to the mailing list <em>%(listname)s</em>.
+ Your subscription settings are shown below; make any necessary changes
+ and hit <em>Subscribe to list ...</em> to complete the confirmation
+ process. Once you've confirmed your subscription request, the
+ moderator must approve or reject your membership request. You will
+ receive notice of their decision.
+
+ <p>Note: your password will be emailed to you once your subscription
+ is confirmed. You can change it by visiting your personal options
+ page.
+
+ <p>Or, if you've changed your mind and do not want to subscribe to
+ this mailing list, you can hit <em>Cancel my subscription
+ request</em>.""") + '<p><hr>'
+ table.AddRow([result])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+
+ table.AddRow([Label(_('Your email address:')), email])
+ table.AddRow([Label(_('Your real name:')),
+ TextBox('realname', name)])
+## table.AddRow([Label(_('Password:')),
+## PasswordBox('password', password)])
+## table.AddRow([Label(_('Password (confirm):')),
+## PasswordBox('pwconfirm', password)])
+ # Only give them a choice to receive digests if they actually have a
+ # choice <wink>.
+ if mlist.nondigestable and mlist.digestable:
+ table.AddRow([Label(_('Receive digests?')),
+ RadioButtonArray('digests', (_('No'), _('Yes')),
+ checked=digest, values=(0, 1))])
+ langs = mlist.GetAvailableLanguages()
+ values = [_(Utils.GetLanguageDescr(l)) for l in langs]
+ try:
+ selected = langs.index(lang)
+ except ValueError:
+ selected = lang.index(mlist.preferred_language)
+ table.AddRow([Label(_('Preferred language:')),
+ SelectOptions('language', langs, values, selected)])
+ table.AddRow([Hidden('cookie', cookie)])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([
+ Label(SubmitButton('cancel', _('Cancel my subscription request'))),
+ SubmitButton('submit', _('Subscribe to list %(listname)s'))
+ ])
+ form.AddItem(table)
+ doc.AddItem(form)
+
+
+
+def subscription_cancel(mlist, doc, cookie):
+ # Discard this cookie
+ userdesc = Pending.confirm(cookie, expunge=1)[1]
+ lang = userdesc.language
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ doc.AddItem(_('You have canceled your subscription request.'))
+
+
+
+def subscription_confirm(mlist, doc, cookie, cgidata):
+ # See the comment in admin.py about the need for the signal
+ # handler.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ listname = mlist.real_name
+ mlist.Lock()
+ try:
+ try:
+ # Some pending values may be overridden in the form. email of
+ # course is hardcoded. ;)
+ lang = cgidata.getvalue('language')
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ if cgidata.has_key('digests'):
+ try:
+ digest = int(cgidata.getvalue('digests'))
+ except ValueError:
+ digest = None
+ else:
+ digest = None
+ userdesc = Pending.confirm(cookie, expunge=0)[1]
+ fullname = cgidata.getvalue('realname', None)
+ if fullname is not None:
+ fullname = Utils.canonstr(fullname, lang)
+ overrides = UserDesc(fullname=fullname, digest=digest, lang=lang)
+ userdesc += overrides
+ op, addr, pw, digest, lang = mlist.ProcessConfirmation(
+ cookie, userdesc)
+ except Errors.MMNeedApproval:
+ title = _('Awaiting moderator approval')
+ doc.SetTitle(title)
+ doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+ doc.AddItem(_("""\
+ You have successfully confirmed your subscription request to the
+ mailing list %(listname)s, however final approval is required from
+ the list moderator before you will be subscribed. Your request
+ has been forwarded to the list moderator, and you will be notified
+ of the moderator's decision."""))
+ except Errors.NotAMemberError:
+ bad_confirmation(doc, _('''Invalid confirmation string. It is
+ possible that you are attempting to confirm a request for an
+ address that has already been unsubscribed.'''))
+ except Errors.MMAlreadyAMember:
+ doc.addError(_("You are already a member of this mailing list!"))
+ else:
+ # Use the user's preferred language
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ # The response
+ listname = mlist.real_name
+ title = _('Subscription request confirmed')
+ optionsurl = mlist.GetOptionsURL(addr, absolute=1)
+ doc.SetTitle(title)
+ doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+ doc.AddItem(_('''\
+ You have successfully confirmed your subscription request for
+ "%(addr)s" to the %(listname)s mailing list. A separate
+ confirmation message will be sent to your email address, along
+ with your password, and other useful information and links.
+
+ <p>You can now
+ <a href="%(optionsurl)s">proceed to your membership login
+ page</a>.'''))
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def unsubscription_cancel(mlist, doc, cookie):
+ # Discard this cookie
+ Pending.confirm(cookie, expunge=1)
+ doc.AddItem(_('You have canceled your unsubscription request.'))
+
+
+
+def unsubscription_confirm(mlist, doc, cookie):
+ # See the comment in admin.py about the need for the signal
+ # handler.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ mlist.Lock()
+ try:
+ try:
+ # Do this in two steps so we can get the preferred language for
+ # the user who is unsubscribing.
+ op, addr = Pending.confirm(cookie, expunge=0)
+ lang = mlist.getMemberLanguage(addr)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ op, addr = mlist.ProcessConfirmation(cookie)
+ except Errors.NotAMemberError:
+ bad_confirmation(doc, _('''Invalid confirmation string. It is
+ possible that you are attempting to confirm a request for an
+ address that has already been unsubscribed.'''))
+ else:
+ # The response
+ listname = mlist.real_name
+ title = _('Unsubscription request confirmed')
+ listinfourl = mlist.GetScriptURL('listinfo', absolute=1)
+ doc.SetTitle(title)
+ doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+ doc.AddItem(_("""\
+ You have successfully unsubscribed from the %(listname)s mailing
+ list. You can now <a href="%(listinfourl)s">visit the list's main
+ information page</a>."""))
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def unsubscription_prompt(mlist, doc, cookie, addr):
+ title = _('Confirm unsubscription request')
+ doc.SetTitle(title)
+ lang = mlist.getMemberLanguage(addr)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+
+ form = Form(mlist.GetScriptURL('confirm', 1))
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ colspan=2, bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ listname = mlist.real_name
+ fullname = mlist.getMemberName(addr)
+ if fullname is None:
+ fullname = _('<em>Not available</em>')
+ else:
+ fullname = Utils.uncanonstr(fullname, lang)
+ table.AddRow([_("""Your confirmation is required in order to complete the
+ unsubscription request from the mailing list <em>%(listname)s</em>. You
+ are currently subscribed with
+
+ <ul><li><b>Real name:</b> %(fullname)s
+ <li><b>Email address:</b> %(addr)s
+ </ul>
+
+ Hit the <em>Unsubscribe</em> button below to complete the confirmation
+ process.
+
+ <p>Or hit <em>Cancel and discard</em> to cancel this unsubscription
+ request.""") + '<p><hr>'])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Hidden('cookie', cookie)])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([SubmitButton('submit', _('Unsubscribe')),
+ SubmitButton('cancel', _('Cancel and discard'))])
+
+ form.AddItem(table)
+ doc.AddItem(form)
+
+
+
+def addrchange_cancel(mlist, doc, cookie):
+ # Discard this cookie
+ Pending.confirm(cookie, expunge=1)
+ doc.AddItem(_('You have canceled your change of address request.'))
+
+
+
+def addrchange_confirm(mlist, doc, cookie):
+ # See the comment in admin.py about the need for the signal
+ # handler.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ mlist.Lock()
+ try:
+ try:
+ # Do this in two steps so we can get the preferred language for
+ # the user who is unsubscribing.
+ op, oldaddr, newaddr, globally = Pending.confirm(cookie, expunge=0)
+ lang = mlist.getMemberLanguage(oldaddr)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ op, oldaddr, newaddr = mlist.ProcessConfirmation(cookie)
+ except Errors.NotAMemberError:
+ bad_confirmation(doc, _('''Invalid confirmation string. It is
+ possible that you are attempting to confirm a request for an
+ address that has already been unsubscribed.'''))
+ else:
+ # The response
+ listname = mlist.real_name
+ title = _('Change of address request confirmed')
+ optionsurl = mlist.GetOptionsURL(newaddr, absolute=1)
+ doc.SetTitle(title)
+ doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+ doc.AddItem(_("""\
+ You have successfully changed your address on the %(listname)s
+ mailing list from <b>%(oldaddr)s</b> to <b>%(newaddr)s</b>. You
+ can now <a href="%(optionsurl)s">proceed to your membership
+ login page</a>."""))
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def addrchange_prompt(mlist, doc, cookie, oldaddr, newaddr, globally):
+ title = _('Confirm change of address request')
+ doc.SetTitle(title)
+ lang = mlist.getMemberLanguage(oldaddr)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+
+ form = Form(mlist.GetScriptURL('confirm', 1))
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ colspan=2, bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ listname = mlist.real_name
+ fullname = mlist.getMemberName(oldaddr)
+ if fullname is None:
+ fullname = _('<em>Not available</em>')
+ else:
+ fullname = Utils.uncanonstr(fullname, lang)
+ if globally:
+ globallys = _('globally')
+ else:
+ globallys = ''
+ table.AddRow([_("""Your confirmation is required in order to complete the
+ change of address request for the mailing list <em>%(listname)s</em>. You
+ are currently subscribed with
+
+ <ul><li><b>Real name:</b> %(fullname)s
+ <li><b>Old email address:</b> %(oldaddr)s
+ </ul>
+
+ and you have requested to %(globallys)s change your email address to
+
+ <ul><li><b>New email address:</b> %(newaddr)s
+ </ul>
+
+ Hit the <em>Change address</em> button below to complete the confirmation
+ process.
+
+ <p>Or hit <em>Cancel and discard</em> to cancel this change of address
+ request.""") + '<p><hr>'])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Hidden('cookie', cookie)])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([SubmitButton('submit', _('Change address')),
+ SubmitButton('cancel', _('Cancel and discard'))])
+
+ form.AddItem(table)
+ doc.AddItem(form)
+
+
+
+def heldmsg_cancel(mlist, doc, cookie):
+ # Discard this cookie
+ title = _('Continue awaiting approval')
+ doc.SetTitle(title)
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ Pending.confirm(cookie, expunge=1)
+ table.AddRow([_('''Okay, the list moderator will still have the
+ opportunity to approve or reject this message.''')])
+ doc.AddItem(table)
+
+
+
+def heldmsg_confirm(mlist, doc, cookie):
+ # See the comment in admin.py about the need for the signal
+ # handler.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ mlist.Lock()
+ try:
+ try:
+ # Do this in two steps so we can get the preferred language for
+ # the user who posted the message.
+ op, id = Pending.confirm(cookie, expunge=1)
+ ign, sender, msgsubject, ign, ign, ign = mlist.GetRecord(id)
+ subject = Utils.websafe(msgsubject)
+ lang = mlist.getMemberLanguage(sender)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ # Discard the message
+ mlist.HandleRequest(id, mm_cfg.DISCARD,
+ _('Sender discarded message via web.'))
+ except Errors.LostHeldMessage:
+ bad_confirmation(doc, _('''The held message with the Subject:
+ header <em>%(subject)s</em> could not be found. The most likely
+ reason for this is that the list moderator has already approved or
+ rejected the message. You were not able to cancel it in
+ time.'''))
+ else:
+ # The response
+ listname = mlist.real_name
+ title = _('Posted message canceled')
+ doc.SetTitle(title)
+ doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+ doc.AddItem(_('''\
+ You have successfully canceled the posting of your message with
+ the Subject: header <em>%(subject)s</em> to the mailing list
+ %(listname)s.'''))
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def heldmsg_prompt(mlist, doc, cookie, id):
+ title = _('Cancel held message posting')
+ doc.SetTitle(title)
+ form = Form(mlist.GetScriptURL('confirm', 1))
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ colspan=2, bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ # Blarg. The list must be locked in order to interact with the ListAdmin
+ # database, even for read-only. See the comment in admin.py about the
+ # need for the signal handler.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+ # Get the record, but watch for KeyErrors which mean the admin has already
+ # disposed of this message.
+ mlist.Lock()
+ try:
+ try:
+ data = mlist.GetRecord(id)
+ except KeyError:
+ data = None
+ finally:
+ mlist.Unlock()
+
+ if data is None:
+ bad_confirmation(doc, _("""The held message you were referred to has
+ already been handled by the list administrator."""))
+ return
+
+ # Unpack the data and present the confirmation message
+ ign, sender, msgsubject, givenreason, ign, ign = data
+ # Now set the language to the sender's preferred.
+ lang = mlist.getMemberLanguage(sender)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+
+ subject = Utils.websafe(msgsubject)
+ reason = Utils.websafe(_(givenreason))
+ listname = mlist.real_name
+ table.AddRow([_('''Your confirmation is required in order to cancel the
+ posting of your message to the mailing list <em>%(listname)s</em>:
+
+ <ul><li><b>Sender:</b> %(sender)s
+ <li><b>Subject:</b> %(subject)s
+ <li><b>Reason:</b> %(reason)s
+ </ul>
+
+ Hit the <em>Cancel posting</em> button to discard the posting.
+
+ <p>Or hit the <em>Continue awaiting approval</em> button to continue to
+ allow the list moderator to approve or reject the message.''')
+ + '<p><hr>'])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Hidden('cookie', cookie)])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([SubmitButton('submit', _('Cancel posting')),
+ SubmitButton('cancel', _('Continue awaiting approval'))])
+
+ form.AddItem(table)
+ doc.AddItem(form)
+
+
+
+def reenable_cancel(mlist, doc, cookie):
+ # Don't actually discard this cookie, since the user may decide to
+ # re-enable their membership at a future time, and we may be sending out
+ # future notifications with this cookie value.
+ doc.AddItem(_("""You have canceled the re-enabling of your membership. If
+ we continue to receive bounces from your address, it could be deleted from
+ this mailing list."""))
+
+
+
+def reenable_confirm(mlist, doc, cookie):
+ # See the comment in admin.py about the need for the signal
+ # handler.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ mlist.Lock()
+ try:
+ try:
+ # Do this in two steps so we can get the preferred language for
+ # the user who is unsubscribing.
+ op, listname, addr = Pending.confirm(cookie, expunge=0)
+ lang = mlist.getMemberLanguage(addr)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+ op, addr = mlist.ProcessConfirmation(cookie)
+ except Errors.NotAMemberError:
+ bad_confirmation(doc, _('''Invalid confirmation string. It is
+ possible that you are attempting to confirm a request for an
+ address that has already been unsubscribed.'''))
+ else:
+ # The response
+ listname = mlist.real_name
+ title = _('Membership re-enabled.')
+ optionsurl = mlist.GetOptionsURL(addr, absolute=1)
+ doc.SetTitle(title)
+ doc.AddItem(Header(3, Bold(FontAttr(title, size='+2'))))
+ doc.AddItem(_("""\
+ You have successfully re-enabled your membership in the
+ %(listname)s mailing list. You can now <a
+ href="%(optionsurl)s">visit your member options page</a>.
+ """))
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def reenable_prompt(mlist, doc, cookie, list, member):
+ title = _('Re-enable mailing list membership')
+ doc.SetTitle(title)
+ form = Form(mlist.GetScriptURL('confirm', 1))
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ colspan=2, bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ lang = mlist.getMemberLanguage(member)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+
+ realname = mlist.real_name
+ info = mlist.getBounceInfo(member)
+ if not info:
+ listinfourl = mlist.GetScriptURL('listinfo', absolute=1)
+ # They've already be unsubscribed
+ table.AddRow([_("""We're sorry, but you have already been unsubscribed
+ from this mailing list. To re-subscribe, please visit the
+ <a href="%(listinfourl)s">list information page</a>.""")])
+ return
+
+ date = time.strftime('%A, %B %d, %Y', info.date + (0,) * 6)
+ daysleft = int(info.noticesleft *
+ mlist.bounce_you_are_disabled_warnings_interval /
+ mm_cfg.days(1))
+ # BAW: for consistency this should be changed to 'fullname' or the above
+ # 'fullname's should be changed to 'username'. Don't want to muck with
+ # the i18n catalogs though.
+ username = mlist.getMemberName(member)
+ if username is None:
+ username = _('<em>not available</em>')
+ else:
+ username = Utils.uncanonstr(username, lang)
+
+ table.AddRow([_("""Your membership in the %(realname)s mailing list is
+ currently disabled due to excessive bounces. Your confirmation is
+ required in order to re-enable delivery to your address. We have the
+ following information on file:
+
+ <ul><li><b>Member address:</b> %(member)s
+ <li><b>Member name:</b> %(username)s
+ <li><b>Last bounce received on:</b> %(date)s
+ <li><b>Approximate number of days before you are permanently removed
+ from this list:</b> %(daysleft)s
+ </ul>
+
+ Hit the <em>Re-enable membership</em> button to resume receiving postings
+ from the mailing list. Or hit the <em>Cancel</em> button to defer
+ re-enabling your membership.
+ """)])
+
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Hidden('cookie', cookie)])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([SubmitButton('submit', _('Re-enable membership')),
+ SubmitButton('cancel', _('Cancel'))])
+
+ form.AddItem(table)
+ doc.AddItem(form)
diff --git a/Mailman/Cgi/create.py b/Mailman/Cgi/create.py
new file mode 100644
index 00000000..31e16269
--- /dev/null
+++ b/Mailman/Cgi/create.py
@@ -0,0 +1,410 @@
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Create mailing lists through the web."""
+
+import sys
+import os
+import signal
+import cgi
+import sha
+from types import ListType
+
+from Mailman import mm_cfg
+from Mailman import MailList
+from Mailman import Message
+from Mailman import Errors
+from Mailman import i18n
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def main():
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ cgidata = cgi.FieldStorage()
+ parts = Utils.GetPathPieces()
+ if parts:
+ # Bad URL specification
+ title = _('Bad URL specification')
+ doc.SetTitle(title)
+ doc.AddItem(
+ Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
+ syslog('error', 'Bad URL specification: %s', parts)
+ elif cgidata.has_key('doit'):
+ # We must be processing the list creation request
+ process_request(doc, cgidata)
+ elif cgidata.has_key('clear'):
+ request_creation(doc)
+ else:
+ # Put up the list creation request form
+ request_creation(doc)
+ doc.AddItem('<hr>')
+ # Always add the footer and print the document
+ doc.AddItem(_('Return to the ') +
+ Link(Utils.ScriptURL('listinfo'),
+ _('general list overview')).Format())
+ doc.AddItem(_('<br>Return to the ') +
+ Link(Utils.ScriptURL('admin'),
+ _('administrative list overview')).Format())
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+
+
+
+def process_request(doc, cgidata):
+ # Lowercase the listname since this is treated as the "internal" name.
+ listname = cgidata.getvalue('listname', '').strip().lower()
+ owner = cgidata.getvalue('owner', '').strip()
+ try:
+ autogen = int(cgidata.getvalue('autogen', '0'))
+ except ValueError:
+ autogen = 0
+ try:
+ notify = int(cgidata.getvalue('notify', '0'))
+ except ValueError:
+ notify = 0
+ try:
+ moderate = int(cgidata.getvalue('moderate', '0'))
+ except ValueError:
+ moderate = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION
+
+ password = cgidata.getvalue('password', '').strip()
+ confirm = cgidata.getvalue('confirm', '').strip()
+ auth = cgidata.getvalue('auth', '').strip()
+ langs = cgidata.getvalue('langs', [mm_cfg.DEFAULT_SERVER_LANGUAGE])
+
+ if type(langs) <> ListType:
+ langs = [langs]
+ # Sanity check
+ if '@' in listname:
+ request_creation(doc, cgidata,
+ _('List name must not include "@": %(listname)s'))
+ return
+ if Utils.list_exists(listname):
+ # BAW: should we tell them the list already exists? This could be
+ # used to mine/guess the existance of non-advertised lists. Then
+ # again, that can be done in other ways already, so oh well.
+ request_creation(doc, cgidata, _('List already exists: %(listname)s'))
+ return
+ if not listname:
+ request_creation(doc, cgidata,
+ _('You forgot to enter the list name'))
+ return
+ if not owner:
+ request_creation(doc, cgidata,
+ _('You forgot to specify the list owner'))
+ return
+
+ if autogen:
+ if password or confirm:
+ request_creation(
+ doc, cgidata,
+ _('''Leave the initial password (and confirmation) fields
+ blank if you want Mailman to autogenerate the list
+ passwords.'''))
+ return
+ password = confirm = Utils.MakeRandomPassword(length=8)
+ else:
+ if password <> confirm:
+ request_creation(doc, cgidata,
+ _('Initial list passwords do not match'))
+ return
+ if not password:
+ request_creation(
+ doc, cgidata,
+ # The little <!-- ignore --> tag is used so that this string
+ # differs from the one in bin/newlist. The former is destined
+ # for the web while the latter is destined for email, so they
+ # must be different entries in the message catalog.
+ _('The list password cannot be empty<!-- ignore -->'))
+ return
+ # The authorization password must be non-empty, and it must match either
+ # the list creation password or the site admin password
+ ok = 0
+ if auth:
+ ok = Utils.check_global_password(auth, 0)
+ if not ok:
+ ok = Utils.check_global_password(auth)
+ if not ok:
+ request_creation(
+ doc, cgidata,
+ _('You are not authorized to create new mailing lists'))
+ return
+ # We've got all the data we need, so go ahead and try to create the list
+ # See admin.py for why we need to set up the signal handler.
+ mlist = MailList.MailList()
+
+ def sigterm_handler(signum, frame, mlist=mlist):
+ # Make sure the list gets unlocked...
+ mlist.Unlock()
+ # ...and ensure we exit, otherwise race conditions could cause us to
+ # enter MailList.Save() while we're in the unlocked state, and that
+ # could be bad!
+ sys.exit(0)
+
+ try:
+ # Install the emergency shutdown signal handler
+ signal.signal(signal.SIGTERM, sigterm_handler)
+
+ pw = sha.new(password).hexdigest()
+ # Guarantee that all newly created files have the proper permission.
+ # proper group ownership should be assured by the autoconf script
+ # enforcing that all directories have the group sticky bit set
+ oldmask = os.umask(002)
+ try:
+ try:
+ mlist.Create(listname, owner, pw, langs)
+ finally:
+ os.umask(oldmask)
+ except Errors.MMBadEmailError, s:
+ request_creation(doc, cgidata,
+ _('Bad owner email address: %(s)s'))
+ return
+ except Errors.MMListAlreadyExistsError:
+ request_creation(doc, cgidata,
+ _('List already exists: %(listname)s'))
+ return
+ except Errors.BadListNameError, s:
+ request_creation(doc, cgidata,
+ _('Illegal list name: %(s)s'))
+ return
+ except Errors.MMListError:
+ request_creation(
+ doc, cgidata,
+ _('''Some unknown error occurred while creating the list.
+ Please contact the site administrator for assistance.'''))
+ return
+
+ # Initialize the host_name and web_page_url attributes, based on
+ # virtual hosting settings and the request environment variables.
+ hostname = Utils.get_domain()
+ mlist.default_member_moderation = moderate
+ mlist.web_page_url = mm_cfg.DEFAULT_URL_PATTERN % hostname
+ mlist.host_name = mm_cfg.VIRTUAL_HOSTS.get(
+ hostname, mm_cfg.DEFAULT_EMAIL_HOST)
+ mlist.Save()
+ finally:
+ # Now be sure to unlock the list. It's okay if we get a signal here
+ # because essentially, the signal handler will do the same thing. And
+ # unlocking is unconditional, so it's not an error if we unlock while
+ # we're already unlocked.
+ mlist.Unlock()
+
+ # Now do the MTA-specific list creation tasks
+ if mm_cfg.MTA:
+ modname = 'Mailman.MTA.' + mm_cfg.MTA
+ __import__(modname)
+ sys.modules[modname].create(mlist, cgi=1)
+
+ # And send the notice to the list owner.
+ if notify:
+ siteadmin = Utils.get_site_email(mlist.host_name, 'admin')
+ text = Utils.maketext(
+ 'newlist.txt',
+ {'listname' : listname,
+ 'password' : password,
+ 'admin_url' : mlist.GetScriptURL('admin', absolute=1),
+ 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=1),
+ 'requestaddr' : mlist.GetRequestEmail(),
+ 'siteowner' : siteadmin,
+ }, mlist=mlist)
+ msg = Message.UserNotification(
+ owner, siteadmin,
+ _('Your new mailing list: %(listname)s'),
+ text, mlist.preferred_language)
+ msg.send(mlist)
+
+ # Success!
+ listinfo_url = mlist.GetScriptURL('listinfo', absolute=1)
+ admin_url = mlist.GetScriptURL('admin', absolute=1)
+ create_url = Utils.ScriptURL('create')
+
+ title = _('Mailing list creation results')
+ doc.SetTitle(title)
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ table.AddRow([_('''You have successfully created the mailing list
+ <b>%(listname)s</b> and notification has been sent to the list owner
+ <b>%(owner)s</b>. You can now:''')])
+ ullist = UnorderedList()
+ ullist.AddItem(Link(listinfo_url, _("Visit the list's info page")))
+ ullist.AddItem(Link(admin_url, _("Visit the list's admin page")))
+ ullist.AddItem(Link(create_url, _('Create another list')))
+ table.AddRow([ullist])
+ doc.AddItem(table)
+
+
+
+# Because the cgi module blows
+class Dummy:
+ def getvalue(self, name, default):
+ return default
+dummy = Dummy()
+
+
+
+def request_creation(doc, cgidata=dummy, errmsg=None):
+ # What virtual domain are we using?
+ hostname = Utils.get_domain()
+ # Set up the document
+ title = _('Create a %(hostname)s Mailing List')
+ doc.SetTitle(title)
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ # Add any error message
+ if errmsg:
+ table.AddRow([Header(3, Bold(
+ FontAttr(_('Error: '), color='#ff0000', size='+2').Format() +
+ Italic(errmsg).Format()))])
+ table.AddRow([_("""You can create a new mailing list by entering the
+ relevant information into the form below. The name of the mailing list
+ will be used as the primary address for posting messages to the list, so
+ it should be lowercased. You will not be able to change this once the
+ list is created.
+
+ <p>You also need to enter the email address of the initial list owner.
+ Once the list is created, the list owner will be given notification, along
+ with the initial list password. The list owner will then be able to
+ modify the password and add or remove additional list owners.
+
+ <p>If you want Mailman to automatically generate the initial list admin
+ password, click on `Yes' in the autogenerate field below, and leave the
+ initial list password fields empty.
+
+ <p>You must have the proper authorization to create new mailing lists.
+ Each site should have a <em>list creator's</em> password, which you can
+ enter in the field at the bottom. Note that the site administrator's
+ password can also be used for authentication.
+ """)])
+ # Build the form for the necessary input
+ GREY = mm_cfg.WEB_ADMINITEM_COLOR
+ form = Form(Utils.ScriptURL('create'))
+ ftable = Table(border=0, cols='2', width='100%',
+ cellspacing=3, cellpadding=4)
+
+ ftable.AddRow([Center(Italic(_('List Identity')))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
+
+ ftable.AddRow([Label(_('Name of list:')),
+ TextBox('listname', cgidata.getvalue('listname', ''))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow([Label(_('Initial list owner address:')),
+ TextBox('owner', cgidata.getvalue('owner', ''))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ try:
+ autogen = int(cgidata.getvalue('autogen', '0'))
+ except ValueError:
+ autogen = 0
+ ftable.AddRow([Label(_('Auto-generate initial list password?')),
+ RadioButtonArray('autogen', (_('No'), _('Yes')),
+ checked=autogen,
+ values=(0, 1))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow([Label(_('Initial list password:')),
+ PasswordBox('password', cgidata.getvalue('password', ''))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow([Label(_('Confirm initial password:')),
+ PasswordBox('confirm', cgidata.getvalue('confirm', ''))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ try:
+ notify = int(cgidata.getvalue('notify', '1'))
+ except ValueError:
+ notify = 1
+
+ ftable.AddRow([Center(Italic(_('List Characteristics')))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
+
+ ftable.AddRow([
+ Label(_("""Should new members be quarantined before they
+ are allowed to post unmoderated to this list? Answer <em>Yes</em> to hold
+ new member postings for moderator approval by default.""")),
+ RadioButtonArray('moderate', (_('No'), _('Yes')),
+ checked=mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION,
+ values=(0,1))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+ # Create the table of initially supported languages, sorted on the long
+ # name of the language.
+ revmap = {}
+ for key, (name, charset) in mm_cfg.LC_DESCRIPTIONS.items():
+ revmap[_(name)] = key
+ langnames = revmap.keys()
+ langnames.sort()
+ langs = []
+ for name in langnames:
+ langs.append(revmap[name])
+ try:
+ langi = langs.index(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+ except ValueError:
+ # Someone must have deleted the servers's preferred language. Could
+ # be other trouble lurking!
+ langi = 0
+ # BAW: we should preserve the list of checked languages across form
+ # invocations.
+ checked = [0] * len(langs)
+ checked[langi] = 1
+ deflang = _(Utils.GetLanguageDescr(mm_cfg.DEFAULT_SERVER_LANGUAGE))
+ ftable.AddRow([Label(_(
+ '''Initial list of supported languages. <p>Note that if you do not
+ select at least one initial language, the list will use the server
+ default language of %(deflang)s''')),
+ CheckBoxArray('langs',
+ [_(Utils.GetLanguageDescr(L)) for L in langs],
+ checked=checked,
+ values=langs)])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow([Label(_('Send "list created" email to list owner?')),
+ RadioButtonArray('notify', (_('No'), _('Yes')),
+ checked=notify,
+ values=(0, 1))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow(['<hr>'])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
+ ftable.AddRow([Label(_("List creator's (authentication) password:")),
+ PasswordBox('auth')])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow([Center(SubmitButton('doit', _('Create List'))),
+ Center(SubmitButton('clear', _('Clear Form')))])
+ form.AddItem(ftable)
+ table.AddRow([form])
+ doc.AddItem(table)
diff --git a/Mailman/Cgi/edithtml.py b/Mailman/Cgi/edithtml.py
new file mode 100644
index 00000000..cd235162
--- /dev/null
+++ b/Mailman/Cgi/edithtml.py
@@ -0,0 +1,170 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Script which implements admin editing of the list's html templates."""
+
+import os
+import cgi
+import errno
+
+from Mailman import Utils
+from Mailman import MailList
+from Mailman.htmlformat import *
+from Mailman.HTMLFormatter import HTMLFormatter
+from Mailman import Errors
+from Mailman.Cgi import Auth
+from Mailman.Logging.Syslog import syslog
+from Mailman import i18n
+
+_ = i18n._
+
+
+
+def main():
+ # Trick out pygettext since we want to mark template_data as translatable,
+ # but we don't want to actually translate it here.
+ def _(s):
+ return s
+
+ template_data = (
+ ('listinfo.html', _('General list information page')),
+ ('subscribe.html', _('Subscribe results page')),
+ ('options.html', _('User specific options page')),
+ )
+
+ _ = i18n._
+ doc = Document()
+
+ # Set up the system default language
+ i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ parts = Utils.GetPathPieces()
+ if not parts:
+ doc.AddItem(Header(2, _("List name is required.")))
+ print doc.Format()
+ return
+
+ listname = parts[0].lower()
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ doc.AddItem(Header(2, _('No such list <em>%(safelistname)s</em>')))
+ print doc.Format()
+ syslog('error', 'No such list "%s": %s', listname, e)
+ return
+
+ # Now that we have a valid list, set the language to its default
+ i18n.set_language(mlist.preferred_language)
+ doc.set_language(mlist.preferred_language)
+
+ # Must be authenticated to get any farther
+ cgidata = cgi.FieldStorage()
+
+ # Editing the html for a list is limited to the list admin and site admin.
+ if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
+ mm_cfg.AuthSiteAdmin),
+ cgidata.getvalue('adminpw', '')):
+ if cgidata.has_key('admlogin'):
+ # This is a re-authorization attempt
+ msg = Bold(FontSize('+1', _('Authorization failed.'))).Format()
+ else:
+ msg = ''
+ Auth.loginpage(mlist, 'admin', msg=msg)
+ return
+
+ realname = mlist.real_name
+ if len(parts) > 1:
+ template_name = parts[1]
+ for (template, info) in template_data:
+ if template == template_name:
+ template_info = _(info)
+ doc.SetTitle(_(
+ '%(realname)s -- Edit html for %(template_info)s'))
+ break
+ else:
+ # Avoid cross-site scripting attacks
+ safetemplatename = Utils.websafe(template_name)
+ doc.SetTitle(_('Edit HTML : Error'))
+ doc.AddItem(Header(2, _("%(safetemplatename)s: Invalid template")))
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ return
+ else:
+ doc.SetTitle(_('%(realname)s -- HTML Page Editing'))
+ doc.AddItem(Header(1, _('%(realname)s -- HTML Page Editing')))
+ doc.AddItem(Header(2, _('Select page to edit:')))
+ template_list = UnorderedList()
+ for (template, info) in template_data:
+ l = Link(mlist.GetScriptURL('edithtml') + '/' + template, _(info))
+ template_list.AddItem(l)
+ doc.AddItem(FontSize("+2", template_list))
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ return
+
+ try:
+ if cgidata.keys():
+ ChangeHTML(mlist, cgidata, template_name, doc)
+ FormatHTML(mlist, doc, template_name, template_info)
+ finally:
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+
+
+
+def FormatHTML(mlist, doc, template_name, template_info):
+ doc.AddItem(Header(1,'%s:' % mlist.real_name))
+ doc.AddItem(Header(1, template_info))
+ doc.AddItem('<hr>')
+
+ link = Link(mlist.GetScriptURL('admin'),
+ _('View or edit the list configuration information.'))
+
+ doc.AddItem(FontSize("+1", link))
+ doc.AddItem('<p>')
+ doc.AddItem('<hr>')
+ form = Form(mlist.GetScriptURL('edithtml') + '/' + template_name)
+ text = Utils.websafe(Utils.maketext(template_name, raw=1, mlist=mlist))
+ form.AddItem(TextArea('html_code', text, rows=40, cols=75))
+ form.AddItem('<p>' + _('When you are done making changes...'))
+ form.AddItem(SubmitButton('submit', _('Submit Changes')))
+ doc.AddItem(form)
+
+
+
+def ChangeHTML(mlist, cgi_info, template_name, doc):
+ if not cgi_info.has_key('html_code'):
+ doc.AddItem(Header(3,_("Can't have empty html page.")))
+ doc.AddItem(Header(3,_("HTML Unchanged.")))
+ doc.AddItem('<hr>')
+ return
+ code = cgi_info['html_code'].value
+ langdir = os.path.join(mlist.fullpath(), mlist.preferred_language)
+ # Make sure the directory exists
+ try:
+ os.mkdir(langdir, 02775)
+ except OSError, e:
+ if e.errno <> errno.EEXIST: raise
+ fp = open(os.path.join(langdir, template_name), 'w')
+ try:
+ fp.write(code)
+ finally:
+ fp.close()
+ doc.AddItem(Header(3, _('HTML successfully updated.')))
+ doc.AddItem('<hr>')
diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py
new file mode 100644
index 00000000..d9e4d266
--- /dev/null
+++ b/Mailman/Cgi/listinfo.py
@@ -0,0 +1,206 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Produce listinfo page, primary web entry-point to mailing lists.
+"""
+
+# No lock needed in this script, because we don't change data.
+
+import os
+import cgi
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import i18n
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def main():
+ parts = Utils.GetPathPieces()
+ if not parts:
+ listinfo_overview()
+ return
+
+ listname = parts[0].lower()
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ listinfo_overview(_('No such list <em>%(safelistname)s</em>'))
+ syslog('error', 'No such list "%s": %s', listname, e)
+ return
+
+ # See if the user want to see this page in other language
+ cgidata = cgi.FieldStorage()
+ language = cgidata.getvalue('language', mlist.preferred_language)
+ i18n.set_language(language)
+ list_listinfo(mlist, language)
+
+
+
+def listinfo_overview(msg=''):
+ # Present the general listinfo overview
+ hostname = Utils.get_domain()
+ # Set up the document and assign it the correct language. The only one we
+ # know about at the moment is the server's default.
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ legend = _("%(hostname)s Mailing Lists")
+ doc.SetTitle(legend)
+
+ table = Table(border=0, width="100%")
+ table.AddRow([Center(Header(2, legend))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ # Skip any mailing lists that isn't advertised.
+ advertised = []
+ listnames = Utils.list_names()
+ listnames.sort()
+
+ for name in listnames:
+ mlist = MailList.MailList(name, lock=0)
+ if mlist.advertised:
+ if mm_cfg.VIRTUAL_HOST_OVERVIEW and \
+ mlist.web_page_url.find(hostname) == -1:
+ # List is for different identity of this host - skip it.
+ continue
+ else:
+ advertised.append(mlist)
+
+ if msg:
+ greeting = FontAttr(msg, color="ff5060", size="+1")
+ else:
+ greeting = FontAttr(_('Welcome!'), size='+2')
+
+ welcome = [greeting]
+ mailmanlink = Link(mm_cfg.MAILMAN_URL, _('Mailman')).Format()
+ if not advertised:
+ welcome.extend(
+ _('''<p>There currently are no publicly-advertised
+ %(mailmanlink)s mailing lists on %(hostname)s.'''))
+ else:
+ welcome.append(
+ _('''<p>Below is a listing of all the public mailing lists on
+ %(hostname)s. Click on a list name to get more information about
+ the list, or to subscribe, unsubscribe, and change the preferences
+ on your subscription.'''))
+
+ # set up some local variables
+ adj = msg and _('right') or ''
+ siteowner = Utils.get_site_email()
+ welcome.extend(
+ (_(''' To visit the general information page for an unadvertised list,
+ open a URL similar to this one, but with a '/' and the %(adj)s
+ list name appended.
+ <p>List administrators, you can visit '''),
+ Link(Utils.ScriptURL('admin'),
+ _('the list admin overview page')),
+ _(''' to find the management interface for your list.
+ <p>Send questions or comments to '''),
+ Link('mailto:' + siteowner, siteowner),
+ '.<p>'))
+
+ table.AddRow([apply(Container, welcome)])
+ table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2)
+
+ if advertised:
+ table.AddRow(['&nbsp;', '&nbsp;'])
+ table.AddRow([Bold(FontAttr(_('List'), size='+2')),
+ Bold(FontAttr(_('Description'), size='+2'))
+ ])
+ highlight = 1
+ for mlist in advertised:
+ table.AddRow(
+ [Link(mlist.GetScriptURL('listinfo'), Bold(mlist.real_name)),
+ mlist.description or Italic(_('[no description available]'))])
+ if highlight and mm_cfg.WEB_HIGHLIGHT_COLOR:
+ table.AddRowInfo(table.GetCurrentRowIndex(),
+ bgcolor=mm_cfg.WEB_HIGHLIGHT_COLOR)
+ highlight = not highlight
+
+ doc.AddItem(table)
+ doc.AddItem('<hr>')
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+
+
+
+def list_listinfo(mlist, lang):
+ # Generate list specific listinfo
+ doc = HeadlessDocument()
+ doc.set_language(lang)
+
+ replacements = mlist.GetStandardReplacements(lang)
+
+ if not mlist.digestable or not mlist.nondigestable:
+ replacements['<mm-digest-radio-button>'] = ""
+ replacements['<mm-undigest-radio-button>'] = ""
+ replacements['<mm-digest-question-start>'] = '<!-- '
+ replacements['<mm-digest-question-end>'] = ' -->'
+ else:
+ replacements['<mm-digest-radio-button>'] = mlist.FormatDigestButton()
+ replacements['<mm-undigest-radio-button>'] = \
+ mlist.FormatUndigestButton()
+ replacements['<mm-digest-question-start>'] = ''
+ replacements['<mm-digest-question-end>'] = ''
+ replacements['<mm-plain-digests-button>'] = \
+ mlist.FormatPlainDigestsButton()
+ replacements['<mm-mime-digests-button>'] = mlist.FormatMimeDigestsButton()
+ replacements['<mm-subscribe-box>'] = mlist.FormatBox('email', size=30)
+ replacements['<mm-subscribe-button>'] = mlist.FormatButton(
+ 'email-button', text=_('Subscribe'))
+ replacements['<mm-new-password-box>'] = mlist.FormatSecureBox('pw')
+ replacements['<mm-confirm-password>'] = mlist.FormatSecureBox('pw-conf')
+ replacements['<mm-subscribe-form-start>'] = mlist.FormatFormStart(
+ 'subscribe')
+ # Roster form substitutions
+ replacements['<mm-roster-form-start>'] = mlist.FormatFormStart('roster')
+ replacements['<mm-roster-option>'] = mlist.FormatRosterOptionForUser(lang)
+ # Options form substitutions
+ replacements['<mm-options-form-start>'] = mlist.FormatFormStart('options')
+ replacements['<mm-editing-options>'] = mlist.FormatEditingOption(lang)
+ replacements['<mm-info-button>'] = SubmitButton('UserOptions',
+ _('Edit Options')).Format()
+ # If only one language is enabled for this mailing list, omit the choice
+ # buttons.
+ if len(mlist.GetAvailableLanguages()) == 1:
+ displang = ''
+ else:
+ displang = mlist.FormatButton('displang-button',
+ text = _("View this page in"))
+ replacements['<mm-displang-box>'] = displang
+ replacements['<mm-lang-form-start>'] = mlist.FormatFormStart('listinfo')
+ replacements['<mm-fullname-box>'] = mlist.FormatBox('fullname', size=30)
+
+ # Do the expansion.
+ doc.AddItem(mlist.ParseTags('listinfo.html', replacements, lang))
+ print doc.Format()
+
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py
new file mode 100644
index 00000000..da4562f7
--- /dev/null
+++ b/Mailman/Cgi/options.py
@@ -0,0 +1,950 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Produce and handle the member options."""
+
+import sys
+import os
+import cgi
+import signal
+import urllib
+from types import ListType
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import MemberAdaptor
+from Mailman import i18n
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+SLASH = '/'
+SETLANGUAGE = -1
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def main():
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ parts = Utils.GetPathPieces()
+ lenparts = parts and len(parts)
+ if not parts or lenparts < 1:
+ title = _('CGI script error')
+ doc.SetTitle(title)
+ doc.AddItem(Header(2, title))
+ doc.addError(_('Invalid options to CGI script.'))
+ doc.AddItem('<hr>')
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+ return
+
+ # get the list and user's name
+ listname = parts[0].lower()
+ # open list
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ title = _('CGI script error')
+ doc.SetTitle(title)
+ doc.AddItem(Header(2, title))
+ doc.addError(_('No such list <em>%(safelistname)s</em>'))
+ doc.AddItem('<hr>')
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+ syslog('error', 'No such list "%s": %s\n', listname, e)
+ return
+
+ # The total contents of the user's response
+ cgidata = cgi.FieldStorage(keep_blank_values=1)
+
+ # Set the language for the page. If we're coming from the listinfo cgi,
+ # we might have a 'language' key in the cgi data. That was an explicit
+ # preference to view the page in, so we should honor that here. If that's
+ # not available, use the list's default language.
+ language = cgidata.getvalue('language', mlist.preferred_language)
+ i18n.set_language(language)
+ doc.set_language(language)
+
+ if lenparts < 2:
+ user = cgidata.getvalue('email')
+ if not user:
+ # If we're coming from the listinfo page and we left the email
+ # address field blank, it's not an error. listinfo.html names the
+ # button UserOptions; we can use that as the descriminator.
+ if not cgidata.getvalue('UserOptions'):
+ doc.addError(_('No address given'))
+ loginpage(mlist, doc, None, cgidata)
+ print doc.Format()
+ return
+ else:
+ user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:])))
+
+ # Avoid cross-site scripting attacks
+ safeuser = Utils.websafe(user)
+ # Sanity check the user, but be careful about leaking membership
+ # information when we're using private rosters.
+ if not mlist.isMember(user) and mlist.private_roster == 0:
+ doc.addError(_('No such member: %(safeuser)s.'))
+ loginpage(mlist, doc, None, cgidata)
+ print doc.Format()
+ return
+
+ # Find the case preserved email address (the one the user subscribed with)
+ lcuser = user.lower()
+ try:
+ cpuser = mlist.getMemberCPAddress(lcuser)
+ except Errors.NotAMemberError:
+ # This happens if the user isn't a member but we've got private rosters
+ cpuser = None
+ if lcuser == cpuser:
+ cpuser = None
+
+ # And now we know the user making the request, so set things up to for the
+ # user's stored preferred language, overridden by any form settings for
+ # their new language preference.
+ userlang = cgidata.getvalue('language', mlist.getMemberLanguage(user))
+ doc.set_language(userlang)
+ i18n.set_language(userlang)
+
+ # See if this is VARHELP on topics.
+ varhelp = None
+ if cgidata.has_key('VARHELP'):
+ varhelp = cgidata['VARHELP'].value
+ elif os.environ.get('QUERY_STRING'):
+ # POST methods, even if their actions have a query string, don't get
+ # put into FieldStorage's keys :-(
+ qs = cgi.parse_qs(os.environ['QUERY_STRING']).get('VARHELP')
+ if qs and type(qs) == types.ListType:
+ varhelp = qs[0]
+ if varhelp:
+ topic_details(mlist, doc, user, cpuser, userlang, varhelp)
+ return
+
+ # Are we processing an unsubscription request from the login screen?
+ if cgidata.has_key('login-unsub'):
+ # Because they can't supply a password for unsubscribing, we'll need
+ # to do the confirmation dance.
+ if mlist.isMember(user):
+ mlist.ConfirmUnsubscription(user, userlang)
+ doc.addError(_('The confirmation email has been sent.'), tag='')
+ else:
+ # Not a member
+ if mlist.private_roster == 0:
+ # Public rosters
+ doc.addError(_('No such member: %(safeuser)s.'))
+ else:
+ syslog('mischief',
+ 'Unsub attempt of non-member w/ private rosters: %s',
+ user)
+ doc.addError(_('The confirmation email has been sent.'),
+ tag='')
+ loginpage(mlist, doc, user, cgidata)
+ print doc.Format()
+ return
+
+ # Are we processing a password reminder from the login screen?
+ if cgidata.has_key('login-remind'):
+ if mlist.isMember(user):
+ mlist.MailUserPassword(user)
+ doc.addError(
+ _('A reminder of your password has been emailed to you.'),
+ tag='')
+ else:
+ # Not a member
+ if mlist.private_roster == 0:
+ # Public rosters
+ doc.addError(_('No such member: %(safeuser)s.'))
+ else:
+ syslog('mischief',
+ 'Reminder attempt of non-member w/ private rosters: %s',
+ user)
+ doc.addError(
+ _('A reminder of your password has been emailed to you.'),
+ tag='')
+ loginpage(mlist, doc, user, cgidata)
+ print doc.Format()
+ return
+
+ # Authenticate, possibly using the password supplied in the login page
+ password = cgidata.getvalue('password', '').strip()
+
+ if not mlist.WebAuthenticate((mm_cfg.AuthUser,
+ mm_cfg.AuthListAdmin,
+ mm_cfg.AuthSiteAdmin),
+ password, user):
+ # Not authenticated, so throw up the login page again. If they tried
+ # to authenticate via cgi (instead of cookie), then print an error
+ # message.
+ if cgidata.has_key('password'):
+ doc.addError(_('Authentication failed.'))
+ # So as not to allow membership leakage, prompt for the email
+ # address and the password here.
+ if mlist.private_roster <> 0:
+ syslog('mischief',
+ 'Login failure with private rosters: %s',
+ user)
+ user = None
+ loginpage(mlist, doc, user, cgidata)
+ print doc.Format()
+ return
+
+ # From here on out, the user is okay to view and modify their membership
+ # options. The first set of checks does not require the list to be
+ # locked.
+
+ if cgidata.has_key('logout'):
+ print mlist.ZapCookie(mm_cfg.AuthUser, user)
+ loginpage(mlist, doc, user, cgidata)
+ print doc.Format()
+ return
+
+ if cgidata.has_key('emailpw'):
+ mlist.MailUserPassword(user)
+ options_page(
+ mlist, doc, user, cpuser, userlang,
+ _('A reminder of your password has been emailed to you.'))
+ print doc.Format()
+ return
+
+ if cgidata.has_key('othersubs'):
+ hostname = mlist.host_name
+ title = _('List subscriptions for %(user)s on %(hostname)s')
+ doc.SetTitle(title)
+ doc.AddItem(Header(2, title))
+ doc.AddItem(_('''Click on a link to visit your options page for the
+ requested mailing list.'''))
+
+ # Troll through all the mailing lists that match host_name and see if
+ # the user is a member. If so, add it to the list.
+ onlists = []
+ for gmlist in lists_of_member(mlist, user) + [mlist]:
+ url = gmlist.GetOptionsURL(user)
+ link = Link(url, gmlist.real_name)
+ onlists.append((gmlist.real_name, link))
+ onlists.sort()
+ items = OrderedList(*[link for name, link in onlists])
+ doc.AddItem(items)
+ print doc.Format()
+ return
+
+ if cgidata.has_key('change-of-address'):
+ # We could be changing the user's full name, email address, or both.
+ # Watch out for non-ASCII characters in the member's name.
+ membername = cgidata.getvalue('fullname')
+ # Canonicalize the member's name
+ membername = Utils.canonstr(membername, language)
+ newaddr = cgidata.getvalue('new-address')
+ confirmaddr = cgidata.getvalue('confirm-address')
+
+ oldname = mlist.getMemberName(user)
+ set_address = set_membername = 0
+
+ # See if the user wants to change their email address globally
+ globally = cgidata.getvalue('changeaddr-globally')
+
+ # We will change the member's name under the following conditions:
+ # - membername has a value
+ # - membername has no value, but they /used/ to have a membername
+ if membername and membername <> oldname:
+ # Setting it to a new value
+ set_membername = 1
+ if not membername and oldname:
+ # Unsetting it
+ set_membername = 1
+ # We will change the user's address if both newaddr and confirmaddr
+ # are non-blank, have the same value, and aren't the currently
+ # subscribed email address (when compared case-sensitively). If both
+ # are blank, but membername is set, we ignore it, otherwise we print
+ # an error.
+ msg = ''
+ if newaddr and confirmaddr:
+ if newaddr <> confirmaddr:
+ options_page(mlist, doc, user, cpuser, userlang,
+ _('Addresses did not match!'))
+ print doc.Format()
+ return
+ if newaddr == user:
+ options_page(mlist, doc, user, cpuser, userlang,
+ _('You are already using that email address'))
+ print doc.Format()
+ return
+ # If they're requesting to subscribe an address which is already a
+ # member, and they're /not/ doing it globally, then refuse.
+ # Otherwise, we'll agree to do it globally (with a warning
+ # message) and let ApprovedChangeMemberAddress() handle already a
+ # member issues.
+ if mlist.isMember(newaddr):
+ safenewaddr = Utils.websafe(newaddr)
+ if globally:
+ listname = mlist.real_name
+ msg += _("""\
+The new address you requested %(newaddr)s is already a member of the
+%(listname)s mailing list, however you have also requested a global change of
+address. Upon confirmation, any other mailing list containing the address
+%(user)s will be changed. """)
+ # Don't return
+ else:
+ options_page(
+ mlist, doc, user, cpuser, userlang,
+ _('The new address is already a member: %(newaddr)s'))
+ print doc.Format()
+ return
+ set_address = 1
+ elif (newaddr or confirmaddr) and not set_membername:
+ options_page(mlist, doc, user, cpuser, userlang,
+ _('Addresses may not be blank'))
+ print doc.Format()
+ return
+
+ # Standard sigterm handler.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ if set_address:
+ # Register the pending change after the list is locked
+ msg += _('A confirmation message has been sent to %(newaddr)s. ')
+ mlist.Lock()
+ try:
+ try:
+ mlist.ChangeMemberAddress(user, newaddr, globally)
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+ except Errors.MMBadEmailError:
+ msg = _('Bad email address provided')
+ except Errors.MMHostileAddress:
+ msg = _('Illegal email address provided')
+ except Errors.MMAlreadyAMember:
+ msg = _('%(newaddr)s is already a member of the list.')
+
+ if set_membername:
+ mlist.Lock()
+ try:
+ mlist.ChangeMemberName(user, membername, globally)
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+ msg += _('Member name successfully changed. ')
+
+ options_page(mlist, doc, user, cpuser, userlang, msg)
+ print doc.Format()
+ return
+
+ if cgidata.has_key('changepw'):
+ newpw = cgidata.getvalue('newpw')
+ confirmpw = cgidata.getvalue('confpw')
+ if not newpw or not confirmpw:
+ options_page(mlist, doc, user, cpuser, userlang,
+ _('Passwords may not be blank'))
+ print doc.Format()
+ return
+ if newpw <> confirmpw:
+ options_page(mlist, doc, user, cpuser, userlang,
+ _('Passwords did not match!'))
+ print doc.Format()
+ return
+
+ # See if the user wants to change their passwords globally
+ mlists = [mlist]
+ if cgidata.getvalue('pw-globally'):
+ mlists.extend(lists_of_member(mlist, user))
+
+ for gmlist in mlists:
+ change_password(gmlist, user, newpw, confirmpw)
+
+ # Regenerate the cookie so a re-authorization isn't necessary
+ print mlist.MakeCookie(mm_cfg.AuthUser, user)
+ options_page(mlist, doc, user, cpuser, userlang,
+ _('Password successfully changed.'))
+ print doc.Format()
+ return
+
+ if cgidata.has_key('unsub'):
+ # Was the confirming check box turned on?
+ if not cgidata.getvalue('unsubconfirm'):
+ options_page(
+ mlist, doc, user, cpuser, userlang,
+ _('''You must confirm your unsubscription request by turning
+ on the checkbox below the <em>Unsubscribe</em> button. You
+ have not been unsubscribed!'''))
+ print doc.Format()
+ return
+
+ # Standard signal handler
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ # Okay, zap them. Leave them sitting at the list's listinfo page. We
+ # must own the list lock, and we want to make sure the user (BAW: and
+ # list admin?) is informed of the removal.
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ mlist.Lock()
+ needapproval = 0
+ try:
+ try:
+ mlist.DeleteMember(
+ user, 'via the member options page', userack=1)
+ except Errors.MMNeedApproval:
+ needapproval = 1
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+ # Now throw up some results page, with appropriate links. We can't
+ # drop them back into their options page, because that's gone now!
+ fqdn_listname = mlist.GetListEmail()
+ owneraddr = mlist.GetOwnerEmail()
+ url = mlist.GetScriptURL('listinfo', absolute=1)
+
+ title = _('Unsubscription results')
+ doc.SetTitle(title)
+ doc.AddItem(Header(2, title))
+ if needapproval:
+ doc.AddItem(_("""Your unsubscription request has been received and
+ forwarded on to the list moderators for approval. You will
+ receive notification once the list moderators have made their
+ decision."""))
+ else:
+ doc.AddItem(_("""You have been successfully unsubscribed from the
+ mailing list %(fqdn_listname)s. If you were receiving digest
+ deliveries you may get one more digest. If you have any questions
+ about your unsubscription, please contact the list owners at
+ %(owneraddr)s."""))
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ return
+
+ if cgidata.has_key('options-submit'):
+ # Digest action flags
+ digestwarn = 0
+ cantdigest = 0
+ mustdigest = 0
+
+ newvals = []
+ # First figure out which options have changed. The item names come
+ # from FormatOptionButton() in HTMLFormatter.py
+ for item, flag in (('digest', mm_cfg.Digests),
+ ('mime', mm_cfg.DisableMime),
+ ('dontreceive', mm_cfg.DontReceiveOwnPosts),
+ ('ackposts', mm_cfg.AcknowledgePosts),
+ ('disablemail', mm_cfg.DisableDelivery),
+ ('conceal', mm_cfg.ConcealSubscription),
+ ('remind', mm_cfg.SuppressPasswordReminder),
+ ('rcvtopic', mm_cfg.ReceiveNonmatchingTopics),
+ ('nodupes', mm_cfg.DontReceiveDuplicates),
+ ):
+ try:
+ newval = int(cgidata.getvalue(item))
+ except (TypeError, ValueError):
+ newval = None
+
+ # Skip this option if there was a problem or it wasn't changed.
+ # Note that delivery status is handled separate from the options
+ # flags.
+ if newval is None:
+ continue
+ elif flag == mm_cfg.DisableDelivery:
+ status = mlist.getDeliveryStatus(user)
+ # Here, newval == 0 means enable, newval == 1 means disable
+ if not newval and status <> MemberAdaptor.ENABLED:
+ newval = MemberAdaptor.ENABLED
+ elif newval and status == MemberAdaptor.ENABLED:
+ newval = MemberAdaptor.BYUSER
+ else:
+ continue
+ elif newval == mlist.getMemberOption(user, flag):
+ continue
+ # Should we warn about one more digest?
+ if flag == mm_cfg.Digests and \
+ newval == 0 and mlist.getMemberOption(user, flag):
+ digestwarn = 1
+
+ newvals.append((flag, newval))
+
+ # The user language is handled a little differently
+ if userlang not in mlist.GetAvailableLanguages():
+ newvals.append((SETLANGUAGE, mlist.preferred_language))
+ else:
+ newvals.append((SETLANGUAGE, userlang))
+
+ # Process user selected topics, but don't make the changes to the
+ # MailList object; we must do that down below when the list is
+ # locked.
+ topicnames = cgidata.getvalue('usertopic')
+ if topicnames:
+ # Some topics were selected. topicnames can actually be a string
+ # or a list of strings depending on whether more than one topic
+ # was selected or not.
+ if not isinstance(topicnames, ListType):
+ # Assume it was a bare string, so listify it
+ topicnames = [topicnames]
+ # unquote the topic names
+ topicnames = [urllib.unquote_plus(n) for n in topicnames]
+
+ # The standard sigterm handler (see above)
+ def sigterm_handler(signum, frame, mlist=mlist):
+ mlist.Unlock()
+ sys.exit(0)
+
+ # Now, lock the list and perform the changes
+ mlist.Lock()
+ try:
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ # `values' is a tuple of flags and the web values
+ for flag, newval in newvals:
+ # Handle language settings differently
+ if flag == SETLANGUAGE:
+ mlist.setMemberLanguage(user, newval)
+ # Handle delivery status separately
+ elif flag == mm_cfg.DisableDelivery:
+ mlist.setDeliveryStatus(user, newval)
+ else:
+ try:
+ mlist.setMemberOption(user, flag, newval)
+ except Errors.CantDigestError:
+ cantdigest = 1
+ except Errors.MustDigestError:
+ mustdigest = 1
+ # Set the topics information.
+ mlist.setMemberTopics(user, topicnames)
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+ # A bag of attributes for the global options
+ class Global:
+ enable = None
+ remind = None
+ nodupes = None
+ mime = None
+ def __nonzero__(self):
+ return len(self.__dict__.keys()) > 0
+
+ globalopts = Global()
+
+ # The enable/disable option and the password remind option may have
+ # their global flags sets.
+ if cgidata.getvalue('deliver-globally'):
+ # Yes, this is inefficient, but the list is so small it shouldn't
+ # make much of a difference.
+ for flag, newval in newvals:
+ if flag == mm_cfg.DisableDelivery:
+ globalopts.enable = newval
+ break
+
+ if cgidata.getvalue('remind-globally'):
+ for flag, newval in newvals:
+ if flag == mm_cfg.SuppressPasswordReminder:
+ globalopts.remind = newval
+ break
+
+ if cgidata.getvalue('nodupes-globally'):
+ for flag, newval in newvals:
+ if flag == mm_cfg.DontReceiveDuplicates:
+ globalopts.nodupes = newval
+ break
+
+ if cgidata.getvalue('mime-globally'):
+ for flag, newval in newvals:
+ if flag == mm_cfg.DisableMime:
+ globalopts.mime = newval
+ break
+
+ if globalopts:
+ for gmlist in lists_of_member(mlist, user):
+ global_options(gmlist, user, globalopts)
+
+ # Now print the results
+ if cantdigest:
+ msg = _('''The list administrator has disabled digest delivery for
+ this list, so your delivery option has not been set. However your
+ other options have been set successfully.''')
+ elif mustdigest:
+ msg = _('''The list administrator has disabled non-digest delivery
+ for this list, so your delivery option has not been set. However
+ your other options have been set successfully.''')
+ else:
+ msg = _('You have successfully set your options.')
+
+ if digestwarn:
+ msg += _('You may get one last digest.')
+
+ options_page(mlist, doc, user, cpuser, userlang, msg)
+ print doc.Format()
+ return
+
+ options_page(mlist, doc, user, cpuser, userlang)
+ print doc.Format()
+
+
+
+def options_page(mlist, doc, user, cpuser, userlang, message=''):
+ # The bulk of the document will come from the options.html template, which
+ # includes it's own html armor (head tags, etc.). Suppress the head that
+ # Document() derived pages get automatically.
+ doc.suppress_head = 1
+
+ if mlist.obscure_addresses:
+ presentable_user = Utils.ObscureEmail(user, for_text=1)
+ if cpuser is not None:
+ cpuser = Utils.ObscureEmail(cpuser, for_text=1)
+ else:
+ presentable_user = user
+
+ fullname = Utils.uncanonstr(mlist.getMemberName(user), userlang)
+ if fullname:
+ presentable_user += ', %s' % fullname
+
+ # Do replacements
+ replacements = mlist.GetStandardReplacements(userlang)
+ replacements['<mm-results>'] = Bold(FontSize('+1', message)).Format()
+ replacements['<mm-digest-radio-button>'] = mlist.FormatOptionButton(
+ mm_cfg.Digests, 1, user)
+ replacements['<mm-undigest-radio-button>'] = mlist.FormatOptionButton(
+ mm_cfg.Digests, 0, user)
+ replacements['<mm-plain-digests-button>'] = mlist.FormatOptionButton(
+ mm_cfg.DisableMime, 1, user)
+ replacements['<mm-mime-digests-button>'] = mlist.FormatOptionButton(
+ mm_cfg.DisableMime, 0, user)
+ replacements['<mm-global-mime-button>'] = (
+ CheckBox('mime-globally', 1, checked=0).Format())
+ replacements['<mm-delivery-enable-button>'] = mlist.FormatOptionButton(
+ mm_cfg.DisableDelivery, 0, user)
+ replacements['<mm-delivery-disable-button>'] = mlist.FormatOptionButton(
+ mm_cfg.DisableDelivery, 1, user)
+ replacements['<mm-disabled-notice>'] = mlist.FormatDisabledNotice(user)
+ replacements['<mm-dont-ack-posts-button>'] = mlist.FormatOptionButton(
+ mm_cfg.AcknowledgePosts, 0, user)
+ replacements['<mm-ack-posts-button>'] = mlist.FormatOptionButton(
+ mm_cfg.AcknowledgePosts, 1, user)
+ replacements['<mm-receive-own-mail-button>'] = mlist.FormatOptionButton(
+ mm_cfg.DontReceiveOwnPosts, 0, user)
+ replacements['<mm-dont-receive-own-mail-button>'] = (
+ mlist.FormatOptionButton(mm_cfg.DontReceiveOwnPosts, 1, user))
+ replacements['<mm-dont-get-password-reminder-button>'] = (
+ mlist.FormatOptionButton(mm_cfg.SuppressPasswordReminder, 1, user))
+ replacements['<mm-get-password-reminder-button>'] = (
+ mlist.FormatOptionButton(mm_cfg.SuppressPasswordReminder, 0, user))
+ replacements['<mm-public-subscription-button>'] = (
+ mlist.FormatOptionButton(mm_cfg.ConcealSubscription, 0, user))
+ replacements['<mm-hide-subscription-button>'] = mlist.FormatOptionButton(
+ mm_cfg.ConcealSubscription, 1, user)
+ replacements['<mm-dont-receive-duplicates-button>'] = (
+ mlist.FormatOptionButton(mm_cfg.DontReceiveDuplicates, 1, user))
+ replacements['<mm-receive-duplicates-button>'] = (
+ mlist.FormatOptionButton(mm_cfg.DontReceiveDuplicates, 0, user))
+ replacements['<mm-unsubscribe-button>'] = (
+ mlist.FormatButton('unsub', _('Unsubscribe')) + '<br>' +
+ CheckBox('unsubconfirm', 1, checked=0).Format() +
+ _('<em>Yes, I really want to unsubscribe</em>'))
+ replacements['<mm-new-pass-box>'] = mlist.FormatSecureBox('newpw')
+ replacements['<mm-confirm-pass-box>'] = mlist.FormatSecureBox('confpw')
+ replacements['<mm-change-pass-button>'] = (
+ mlist.FormatButton('changepw', _("Change My Password")))
+ replacements['<mm-other-subscriptions-submit>'] = (
+ mlist.FormatButton('othersubs',
+ _('List my other subscriptions')))
+ replacements['<mm-form-start>'] = (
+ mlist.FormatFormStart('options', user))
+ replacements['<mm-user>'] = user
+ replacements['<mm-presentable-user>'] = presentable_user
+ replacements['<mm-email-my-pw>'] = mlist.FormatButton(
+ 'emailpw', (_('Email My Password To Me')))
+ replacements['<mm-umbrella-notice>'] = (
+ mlist.FormatUmbrellaNotice(user, _("password")))
+ replacements['<mm-logout-button>'] = (
+ mlist.FormatButton('logout', _('Log out')))
+ replacements['<mm-options-submit-button>'] = mlist.FormatButton(
+ 'options-submit', _('Submit My Changes'))
+ replacements['<mm-global-pw-changes-button>'] = (
+ CheckBox('pw-globally', 1, checked=0).Format())
+ replacements['<mm-global-deliver-button>'] = (
+ CheckBox('deliver-globally', 1, checked=0).Format())
+ replacements['<mm-global-remind-button>'] = (
+ CheckBox('remind-globally', 1, checked=0).Format())
+ replacements['<mm-global-nodupes-button>'] = (
+ CheckBox('nodupes-globally', 1, checked=0).Format())
+
+ days = int(mm_cfg.PENDING_REQUEST_LIFE / mm_cfg.days(1))
+ if days > 1:
+ units = _('days')
+ else:
+ units = _('day')
+ replacements['<mm-pending-days>'] = _('%(days)d %(units)s')
+
+ replacements['<mm-new-address-box>'] = mlist.FormatBox('new-address')
+ replacements['<mm-confirm-address-box>'] = mlist.FormatBox(
+ 'confirm-address')
+ replacements['<mm-change-address-button>'] = mlist.FormatButton(
+ 'change-of-address', _('Change My Address and Name'))
+ replacements['<mm-global-change-of-address>'] = CheckBox(
+ 'changeaddr-globally', 1, checked=0).Format()
+ replacements['<mm-fullname-box>'] = mlist.FormatBox(
+ 'fullname', value=fullname)
+
+ # Create the topics radios. BAW: what if the list admin deletes a topic,
+ # but the user still wants to get that topic message?
+ usertopics = mlist.getMemberTopics(user)
+ if mlist.topics:
+ table = Table(border="0")
+ for name, pattern, description, emptyflag in mlist.topics:
+ quotedname = urllib.quote_plus(name)
+ details = Link(mlist.GetScriptURL('options') +
+ '/%s/?VARHELP=%s' % (user, quotedname),
+ ' (Details)')
+ if name in usertopics:
+ checked = 1
+ else:
+ checked = 0
+ table.AddRow([CheckBox('usertopic', quotedname, checked=checked),
+ name + details.Format()])
+ topicsfield = table.Format()
+ else:
+ topicsfield = _('<em>No topics defined</em>')
+ replacements['<mm-topics>'] = topicsfield
+ replacements['<mm-suppress-nonmatching-topics>'] = (
+ mlist.FormatOptionButton(mm_cfg.ReceiveNonmatchingTopics, 0, user))
+ replacements['<mm-receive-nonmatching-topics>'] = (
+ mlist.FormatOptionButton(mm_cfg.ReceiveNonmatchingTopics, 1, user))
+
+ if cpuser is not None:
+ replacements['<mm-case-preserved-user>'] = _('''
+You are subscribed to this list with the case-preserved address
+<em>%(cpuser)s</em>.''')
+ else:
+ replacements['<mm-case-preserved-user>'] = ''
+
+ doc.AddItem(mlist.ParseTags('options.html', replacements, userlang))
+
+
+
+def loginpage(mlist, doc, user, cgidata):
+ realname = mlist.real_name
+ actionurl = mlist.GetScriptURL('options')
+ if user is None:
+ title = _('%(realname)s list: member options login page')
+ extra = _('email address and ')
+ else:
+ title = _('%(realname)s list: member options for user %(user)s')
+ obuser = Utils.ObscureEmail(user)
+ extra = ''
+ # Set up the title
+ doc.SetTitle(title)
+ # We use a subtable here so we can put a language selection box in
+ lang = cgidata.getvalue('language', mlist.preferred_language)
+ table = Table(width='100%', border=0, cellspacing=4, cellpadding=5)
+ # If only one language is enabled for this mailing list, omit the choice
+ # buttons.
+ table.AddRow([Center(Header(2, title))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ if len(mlist.GetAvailableLanguages()) > 1:
+ langform = Form(actionurl)
+ langform.AddItem(SubmitButton('displang-button',
+ _('View this page in')))
+ langform.AddItem(mlist.GetLangSelectBox(lang))
+ if user:
+ langform.AddItem(Hidden('email', user))
+ table.AddRow([Center(langform)])
+ doc.AddItem(table)
+ # Preamble
+ # Set up the login page
+ form = Form(actionurl)
+ table = Table(width='100%', border=0, cellspacing=4, cellpadding=5)
+ table.AddRow([_("""In order to change your membership option, you must
+ first log in by giving your %(extra)smembership password in the section
+ below. If you don't remember your membership password, you can have it
+ emailed to you by clicking on the button below. If you just want to
+ unsubscribe from this list, click on the <em>Unsubscribe</em> button and a
+ confirmation message will be sent to you.
+
+ <p><strong><em>Important:</em></strong> From this point on, you must have
+ cookies enabled in your browser, otherwise none of your changes will take
+ effect.
+ """)])
+ # Password and login button
+ ptable = Table(width='50%', border=0, cellspacing=4, cellpadding=5)
+ if user is None:
+ ptable.AddRow([Label(_('Email address:')),
+ TextBox('email', size=20)])
+ else:
+ ptable.AddRow([Hidden('email', user)])
+ ptable.AddRow([Label(_('Password:')),
+ PasswordBox('password', size=20)])
+ ptable.AddRow([Center(SubmitButton('login', _('Log in')))])
+ ptable.AddCellInfo(ptable.GetCurrentRowIndex(), 0, colspan=2)
+ table.AddRow([Center(ptable)])
+ # Unsubscribe section
+ table.AddRow([Center(Header(2, _('Unsubscribe')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ table.AddRow([_("""By clicking on the <em>Unsubscribe</em> button, a
+ confirmation message will be emailed to you. This message will have a
+ link that you should click on to complete the removal process (you can
+ also confirm by email; see the instructions in the confirmation
+ message).""")])
+
+ table.AddRow([Center(SubmitButton('login-unsub', _('Unsubscribe')))])
+ # Password reminder section
+ table.AddRow([Center(Header(2, _('Password reminder')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ table.AddRow([_("""By clicking on the <em>Remind</em> button, your
+ password will be emailed to you.""")])
+
+ table.AddRow([Center(SubmitButton('login-remind', _('Remind')))])
+ # Finish up glomming together the login page
+ form.AddItem(table)
+ doc.AddItem(form)
+ doc.AddItem(mlist.GetMailmanFooter())
+
+
+
+def lists_of_member(mlist, user):
+ hostname = mlist.host_name
+ onlists = []
+ for listname in Utils.list_names():
+ # The current list will always handle things in the mainline
+ if listname == mlist.internal_name():
+ continue
+ glist = MailList.MailList(listname, lock=0)
+ if glist.host_name <> hostname:
+ continue
+ if not glist.isMember(user):
+ continue
+ onlists.append(glist)
+ return onlists
+
+
+
+def change_password(mlist, user, newpw, confirmpw):
+ # This operation requires the list lock, so let's set up the signal
+ # handling so the list lock will get released when the user hits the
+ # browser stop button.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ # Make sure the list gets unlocked...
+ mlist.Unlock()
+ # ...and ensure we exit, otherwise race conditions could cause us to
+ # enter MailList.Save() while we're in the unlocked state, and that
+ # could be bad!
+ sys.exit(0)
+
+ # Must own the list lock!
+ mlist.Lock()
+ try:
+ # Install the emergency shutdown signal handler
+ signal.signal(signal.SIGTERM, sigterm_handler)
+ # change the user's password. The password must already have been
+ # compared to the confirmpw and otherwise been vetted for
+ # acceptability.
+ mlist.setMemberPassword(user, newpw)
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def global_options(mlist, user, globalopts):
+ # Is there anything to do?
+ for attr in dir(globalopts):
+ if attr.startswith('_'):
+ continue
+ if getattr(globalopts, attr) is not None:
+ break
+ else:
+ return
+
+ def sigterm_handler(signum, frame, mlist=mlist):
+ # Make sure the list gets unlocked...
+ mlist.Unlock()
+ # ...and ensure we exit, otherwise race conditions could cause us to
+ # enter MailList.Save() while we're in the unlocked state, and that
+ # could be bad!
+ sys.exit(0)
+
+ # Must own the list lock!
+ mlist.Lock()
+ try:
+ # Install the emergency shutdown signal handler
+ signal.signal(signal.SIGTERM, sigterm_handler)
+
+ if globalopts.enable is not None:
+ mlist.setDeliveryStatus(user, globalopts.enable)
+
+ if globalopts.remind is not None:
+ mlist.setMemberOption(user, mm_cfg.SuppressPasswordReminder,
+ globalopts.remind)
+
+ if globalopts.nodupes is not None:
+ mlist.setMemberOption(user, mm_cfg.DontReceiveDuplicates,
+ globalopts.nodupes)
+
+ if globalopts.mime is not None:
+ mlist.setMemberOption(user, mm_cfg.DisableMime, globalopts.mime)
+
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def topic_details(mlist, doc, user, cpuser, userlang, varhelp):
+ # Find out which topic the user wants to get details of
+ reflist = varhelp.split('/')
+ name = None
+ topicname = _('<missing>')
+ if len(reflist) == 1:
+ topicname = urllib.unquote_plus(reflist[0])
+ for name, pattern, description, emptyflag in mlist.topics:
+ if name == topicname:
+ break
+ else:
+ name = None
+
+ if not name:
+ options_page(mlist, doc, user, cpuser, userlang,
+ _('Requested topic is not valid: %(topicname)s'))
+ print doc.Format()
+ return
+
+ table = Table(border=3, width='100%')
+ table.AddRow([Center(Bold(_('Topic filter details')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2,
+ bgcolor=mm_cfg.WEB_SUBHEADER_COLOR)
+ table.AddRow([Bold(Label(_('Name:'))),
+ Utils.websafe(name)])
+ table.AddRow([Bold(Label(_('Pattern (as regexp):'))),
+ '<pre>' + Utils.websafe(pattern) + '</pre>'])
+ table.AddRow([Bold(Label(_('Description:'))),
+ Utils.websafe(description)])
+ # Make colors look nice
+ for row in range(1, 4):
+ table.AddCellInfo(row, 0, bgcolor=mm_cfg.WEB_ADMINITEM_COLOR)
+
+ options_page(mlist, doc, user, cpuser, userlang, table.Format())
+ print doc.Format()
diff --git a/Mailman/Cgi/private.py b/Mailman/Cgi/private.py
new file mode 100644
index 00000000..6b7af70a
--- /dev/null
+++ b/Mailman/Cgi/private.py
@@ -0,0 +1,162 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Provide a password-interface wrapper around private archives.
+"""
+
+import sys
+import os
+import cgi
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import i18n
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n. Until we know which list is being requested, we use the
+# server's default.
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def true_path(path):
+ "Ensure that the path is safe by removing .."
+ path = path.replace('../', '')
+ path = path.replace('./', '')
+ return path[1:]
+
+
+def content_type(path):
+ if path[-3:] == '.gz':
+ path = path[:-3]
+ if path[-4:] == '.txt':
+ return 'text/plain'
+ return 'text/html'
+
+
+
+def main():
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ parts = Utils.GetPathPieces()
+ if not parts:
+ doc.SetTitle(_("Private Archive Error"))
+ doc.AddItem(Header(3, _("You must specify a list.")))
+ print doc.Format()
+ return
+
+ path = os.environ.get('PATH_INFO')
+ # BAW: This needs to be converted to the Site module abstraction
+ true_filename = os.path.join(
+ mm_cfg.PRIVATE_ARCHIVE_FILE_DIR,
+ true_path(path))
+
+ listname = parts[0].lower()
+ mboxfile = ''
+ if len(parts) > 1:
+ mboxfile = parts[1]
+
+ # See if it's the list's mbox file is being requested
+ if listname.endswith('.mbox') and mboxfile.endswith('.mbox') and \
+ listname[:-5] == mboxfile[:-5]:
+ listname = listname[:-5]
+ else:
+ mboxfile = ''
+
+ # If it's a directory, we have to append index.html in this script. We
+ # must also check for a gzipped file, because the text archives are
+ # usually stored in compressed form.
+ if os.path.isdir(true_filename):
+ true_filename = true_filename + '/index.html'
+ if not os.path.exists(true_filename) and \
+ os.path.exists(true_filename + '.gz'):
+ true_filename = true_filename + '.gz'
+
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ msg = _('No such list <em>%(safelistname)s</em>')
+ doc.SetTitle(_("Private Archive Error - %(msg)s"))
+ doc.AddItem(Header(2, msg))
+ print doc.Format()
+ syslog('error', 'No such list "%s": %s\n', listname, e)
+ return
+
+ i18n.set_language(mlist.preferred_language)
+ doc.set_language(mlist.preferred_language)
+
+ cgidata = cgi.FieldStorage()
+ username = cgidata.getvalue('username', '')
+ password = cgidata.getvalue('password', '')
+
+ is_auth = 0
+ realname = mlist.real_name
+ message = ''
+
+ if not mlist.WebAuthenticate((mm_cfg.AuthUser,
+ mm_cfg.AuthListModerator,
+ mm_cfg.AuthListAdmin,
+ mm_cfg.AuthSiteAdmin),
+ password, username):
+ if cgidata.has_key('submit'):
+ # This is a re-authorization attempt
+ message = Bold(FontSize('+1', _('Authorization failed.'))).Format()
+ # Output the password form
+ charset = Utils.GetCharSet(mlist.preferred_language)
+ print 'Content-type: text/html; charset=' + charset + '\n\n'
+ while path and path[0] == '/':
+ path=path[1:] # Remove leading /'s
+ print Utils.maketext(
+ 'private.html',
+ {'action' : mlist.GetScriptURL('private', absolute=1),
+ 'realname': mlist.real_name,
+ 'message' : message,
+ }, mlist=mlist)
+ return
+
+ lang = mlist.getMemberLanguage(username)
+ i18n.set_language(lang)
+ doc.set_language(lang)
+
+ # Authorization confirmed... output the desired file
+ try:
+ ctype = content_type(path)
+ if mboxfile:
+ f = open(os.path.join(mlist.archive_dir() + '.mbox',
+ mlist.internal_name() + '.mbox'))
+ ctype = 'text/plain'
+ elif true_filename[-3:] == '.gz':
+ import gzip
+ f = gzip.open(true_filename, 'r')
+ else:
+ f = open(true_filename, 'r')
+ except IOError:
+ msg = _('Private archive file not found')
+ doc.SetTitle(msg)
+ doc.AddItem(Header(2, msg))
+ print doc.Format()
+ syslog('error', 'Private archive file not found: %s', true_filename)
+ else:
+ print 'Content-type: %s\n' % ctype
+ sys.stdout.write(f.read())
+ f.close()
diff --git a/Mailman/Cgi/rmlist.py b/Mailman/Cgi/rmlist.py
new file mode 100644
index 00000000..fab57edd
--- /dev/null
+++ b/Mailman/Cgi/rmlist.py
@@ -0,0 +1,242 @@
+# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Remove/delete mailing lists through the web."""
+
+import os
+import cgi
+import sys
+import errno
+import shutil
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import i18n
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def main():
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ cgidata = cgi.FieldStorage()
+ parts = Utils.GetPathPieces()
+
+ if not parts:
+ # Bad URL specification
+ title = _('Bad URL specification')
+ doc.SetTitle(title)
+ doc.AddItem(
+ Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
+ doc.AddItem('<hr>')
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+ syslog('error', 'Bad URL specification: %s', parts)
+ return
+
+ listname = parts[0].lower()
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ title = _('No such list <em>%(safelistname)s</em>')
+ doc.SetTitle(title)
+ doc.AddItem(
+ Header(3,
+ Bold(FontAttr(title, color='#ff0000', size='+2'))))
+ doc.AddItem('<hr>')
+ doc.AddItem(MailmanLogo())
+ print doc.Format()
+ syslog('error', 'No such list "%s": %s\n', listname, e)
+ return
+
+ # Now that we have a valid mailing list, set the language
+ i18n.set_language(mlist.preferred_language)
+ doc.set_language(mlist.preferred_language)
+
+ # Be sure the list owners are not sneaking around!
+ if not mm_cfg.OWNERS_CAN_DELETE_THEIR_OWN_LISTS:
+ title = _("You're being a sneaky list owner!")
+ doc.SetTitle(title)
+ doc.AddItem(
+ Header(3, Bold(FontAttr(title, color='#ff0000', size='+2'))))
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ syslog('mischief', 'Attempt to sneakily delete a list: %s', listname)
+ return
+
+ if cgidata.has_key('doit'):
+ process_request(doc, cgidata, mlist)
+ print doc.Format()
+ return
+
+ request_deletion(doc, mlist)
+ # Always add the footer and print the document
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+
+
+
+def process_request(doc, cgidata, mlist):
+ password = cgidata.getvalue('password', '').strip()
+ try:
+ delarchives = int(cgidata.getvalue('delarchives', '0'))
+ except ValueError:
+ delarchives = 0
+
+ # Removing a list is limited to the list-creator (a.k.a. list-destroyer),
+ # the list-admin, or the site-admin. Don't use WebAuthenticate here
+ # because we want to be sure the actual typed password is valid, not some
+ # password sitting in a cookie.
+ if mlist.Authenticate((mm_cfg.AuthCreator,
+ mm_cfg.AuthListAdmin,
+ mm_cfg.AuthSiteAdmin),
+ password) == mm_cfg.UnAuthorized:
+ request_deletion(
+ doc, mlist,
+ _('You are not authorized to delete this mailing list'))
+ return
+
+ # Do the MTA-specific list deletion tasks
+ if mm_cfg.MTA:
+ modname = 'Mailman.MTA.' + mm_cfg.MTA
+ __import__(modname)
+ sys.modules[modname].remove(mlist, cgi=1)
+
+ REMOVABLES = ['lists/%s']
+
+ if delarchives:
+ REMOVABLES.extend(['archives/private/%s',
+ 'archives/private/%s.mbox',
+ 'archives/public/%s',
+ 'archives/public/%s.mbox',
+ ])
+
+ problems = 0
+ listname = mlist.internal_name()
+ for dirtmpl in REMOVABLES:
+ dir = os.path.join(mm_cfg.VAR_PREFIX, dirtmpl % listname)
+ if os.path.islink(dir):
+ try:
+ os.unlink(dir)
+ except OSError, e:
+ if e.errno not in (errno.EACCES, errno.EPERM): raise
+ problems += 1
+ syslog('error',
+ 'link %s not deleted due to permission problems',
+ dir)
+ elif os.path.isdir(dir):
+ try:
+ shutil.rmtree(dir)
+ except OSError, e:
+ if e.errno not in (errno.EACCES, errno.EPERM): raise
+ problems += 1
+ syslog('error',
+ 'directory %s not deleted due to permission problems',
+ dir)
+
+ title = _('Mailing list deletion results')
+ doc.SetTitle(title)
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+ if not problems:
+ table.AddRow([_('''You have successfully deleted the mailing list
+ <b>%(listname)s</b>.''')])
+ else:
+ sitelist = Utils.get_site_email(mlist.host_name)
+ table.AddRow([_('''There were some problems deleting the mailing list
+ <b>%(listname)s</b>. Contact your site administrator at %(sitelist)s
+ for details.''')])
+ doc.AddItem(table)
+ doc.AddItem('<hr>')
+ doc.AddItem(_('Return to the ') +
+ Link(Utils.ScriptURL('listinfo'),
+ _('general list overview')).Format())
+ doc.AddItem(_('<br>Return to the ') +
+ Link(Utils.ScriptURL('admin'),
+ _('administrative list overview')).Format())
+ doc.AddItem(MailmanLogo())
+
+
+
+def request_deletion(doc, mlist, errmsg=None):
+ realname = mlist.real_name
+ title = _('Permanently remove mailing list <em>%(realname)s</em>')
+ doc.SetTitle(title)
+
+ table = Table(border=0, width='100%')
+ table.AddRow([Center(Bold(FontAttr(title, size='+1')))])
+ table.AddCellInfo(table.GetCurrentRowIndex(), 0,
+ bgcolor=mm_cfg.WEB_HEADER_COLOR)
+
+ # Add any error message
+ if errmsg:
+ table.AddRow([Header(3, Bold(
+ FontAttr(_('Error: '), color='#ff0000', size='+2').Format() +
+ Italic(errmsg).Format()))])
+
+ table.AddRow([_("""This page allows you as the list owner, to permanent
+ remove this mailing list from the system. <strong>This action is not
+ undoable</strong> so you should undertake it only if you are absolutely
+ sure this mailing list has served its purpose and is no longer necessary.
+
+ <p>Note that no warning will be sent to your list members and after this
+ action, any subsequent messages sent to the mailing list, or any of its
+ administrative addreses will bounce.
+
+ <p>You also have the option of removing the archives for this mailing list
+ at this time. It is almost always recommended that you do
+ <strong>not</strong> remove the archives, since they serve as the
+ historical record of your mailing list.
+
+ <p>For your safety, you will be asked to reconfirm the list password.
+ """)])
+ GREY = mm_cfg.WEB_ADMINITEM_COLOR
+ form = Form(mlist.GetScriptURL('rmlist'))
+ ftable = Table(border=0, cols='2', width='100%',
+ cellspacing=3, cellpadding=4)
+
+ ftable.AddRow([Label(_('List password:')), PasswordBox('password')])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow([Label(_('Also delete archives?')),
+ RadioButtonArray('delarchives', (_('No'), _('Yes')),
+ checked=0, values=(0, 1))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY)
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY)
+
+ ftable.AddRow([Center(Link(
+ mlist.GetScriptURL('admin'),
+ _('<b>Cancel</b> and return to list administration')))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
+
+ ftable.AddRow([Center(SubmitButton('doit', _('Delete this list')))])
+ ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, colspan=2)
+ form.AddItem(ftable)
+ table.AddRow([form])
+ doc.AddItem(table)
diff --git a/Mailman/Cgi/roster.py b/Mailman/Cgi/roster.py
new file mode 100644
index 00000000..71c06240
--- /dev/null
+++ b/Mailman/Cgi/roster.py
@@ -0,0 +1,129 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Produce subscriber roster, using listinfo form data, roster.html template.
+
+Takes listname in PATH_INFO.
+"""
+
+
+# We don't need to lock in this script, because we're never going to change
+# data.
+
+import sys
+import os
+import cgi
+import urllib
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import i18n
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def main():
+ parts = Utils.GetPathPieces()
+ if not parts:
+ error_page(_('Invalid options to CGI script'))
+ return
+
+ listname = parts[0].lower()
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ error_page(_('No such list <em>%(safelistname)s</em>'))
+ syslog('error', 'roster: no such list "%s": %s', listname, e)
+ return
+
+ cgidata = cgi.FieldStorage()
+
+ # messages in form should go in selected language (if any...)
+ if cgidata.has_key('language'):
+ lang = cgidata['language'].value
+ else:
+ lang = mlist.preferred_language
+
+ i18n.set_language(lang)
+
+ # Perform authentication for protected rosters. If the roster isn't
+ # protected, then anybody can see the pages. If members-only or
+ # "admin"-only, then we try to cookie authenticate the user, and failing
+ # that, we check roster-email and roster-pw fields for a valid password.
+ # (also allowed: the list moderator, the list admin, and the site admin).
+ if mlist.private_roster == 0:
+ # No privacy
+ ok = 1
+ elif mlist.private_roster == 1:
+ # Members only
+ addr = cgidata.getvalue('roster-email', '')
+ password = cgidata.getvalue('roster-pw', '')
+ ok = mlist.WebAuthenticate((mm_cfg.AuthUser,
+ mm_cfg.AuthListModerator,
+ mm_cfg.AuthListAdmin,
+ mm_cfg.AuthSiteAdmin),
+ password, addr)
+ else:
+ # Admin only, so we can ignore the address field
+ password = cgidata.getvalue('roster-pw', '')
+ ok = mlist.WebAuthenticate((mm_cfg.AuthListModerator,
+ mm_cfg.AuthListAdmin,
+ mm_cfg.AuthSiteAdmin),
+ password)
+ if not ok:
+ realname = mlist.real_name
+ doc = Document()
+ doc.set_language(lang)
+ error_page_doc(doc, _('%(realname)s roster authentication failed.'))
+ doc.AddItem(mlist.GetMailmanFooter())
+ print doc.Format()
+ return
+
+ # The document and its language
+ doc = HeadlessDocument()
+ doc.set_language(lang)
+
+ replacements = mlist.GetAllReplacements(lang)
+ replacements['<mm-displang-box>'] = mlist.FormatButton(
+ 'displang-button',
+ text = _('View this page in'))
+ replacements['<mm-lang-form-start>'] = mlist.FormatFormStart('roster')
+ doc.AddItem(mlist.ParseTags('roster.html', replacements, lang))
+ print doc.Format()
+
+
+
+def error_page(errmsg):
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+ error_page_doc(doc, errmsg)
+ print doc.Format()
+
+
+def error_page_doc(doc, errmsg, *args):
+ # Produce a simple error-message page on stdout and exit.
+ doc.SetTitle(_("Error"))
+ doc.AddItem(Header(2, _("Error")))
+ doc.AddItem(Bold(errmsg % args))
diff --git a/Mailman/Cgi/subscribe.py b/Mailman/Cgi/subscribe.py
new file mode 100644
index 00000000..c2dfe5cd
--- /dev/null
+++ b/Mailman/Cgi/subscribe.py
@@ -0,0 +1,276 @@
+# Copyright (C) 1998,1999,2000,2001,2002 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Process subscription or roster requests from listinfo form."""
+
+import sys
+import os
+import cgi
+import signal
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import MailList
+from Mailman import Errors
+from Mailman import i18n
+from Mailman import Message
+from Mailman.UserDesc import UserDesc
+from Mailman.htmlformat import *
+from Mailman.Logging.Syslog import syslog
+
+SLASH = '/'
+ERRORSEP = '\n\n<p>'
+
+# Set up i18n
+_ = i18n._
+i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+
+
+def main():
+ doc = Document()
+ doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE)
+
+ parts = Utils.GetPathPieces()
+ if not parts:
+ doc.AddItem(Header(2, _("Error")))
+ doc.AddItem(Bold(_('Invalid options to CGI script')))
+ print doc.Format()
+ return
+
+ listname = parts[0].lower()
+ try:
+ mlist = MailList.MailList(listname, lock=0)
+ except Errors.MMListError, e:
+ # Avoid cross-site scripting attacks
+ safelistname = Utils.websafe(listname)
+ doc.AddItem(Header(2, _("Error")))
+ doc.AddItem(Bold(_('No such list <em>%(safelistname)s</em>')))
+ print doc.Format()
+ syslog('error', 'No such list "%s": %s\n', listname, e)
+ return
+
+ # See if the form data has a preferred language set, in which case, use it
+ # for the results. If not, use the list's preferred language.
+ cgidata = cgi.FieldStorage()
+ language = cgidata.getvalue('language', mlist.preferred_language)
+ i18n.set_language(language)
+ doc.set_language(language)
+
+ # We need a signal handler to catch the SIGTERM that can come from Apache
+ # when the user hits the browser's STOP button. See the comment in
+ # admin.py for details.
+ #
+ # BAW: Strictly speaking, the list should not need to be locked just to
+ # read the request database. However the request database asserts that
+ # the list is locked in order to load it and it's not worth complicating
+ # that logic.
+ def sigterm_handler(signum, frame, mlist=mlist):
+ # Make sure the list gets unlocked...
+ mlist.Unlock()
+ # ...and ensure we exit, otherwise race conditions could cause us to
+ # enter MailList.Save() while we're in the unlocked state, and that
+ # could be bad!
+ sys.exit(0)
+
+ mlist.Lock()
+ try:
+ # Install the emergency shutdown signal handler
+ signal.signal(signal.SIGTERM, sigterm_handler)
+
+ process_form(mlist, doc, cgidata, language)
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+
+def process_form(mlist, doc, cgidata, lang):
+ listowner = mlist.GetOwnerEmail()
+ realname = mlist.real_name
+ results = []
+
+ # The email address being subscribed, required
+ email = cgidata.getvalue('email', '')
+ if not email:
+ results.append(_('You must supply a valid email address.'))
+
+ fullname = cgidata.getvalue('fullname', '')
+ # Canonicalize the full name
+ fullname = Utils.canonstr(fullname, lang)
+ # Who was doing the subscribing?
+ remote = os.environ.get('REMOTE_HOST',
+ os.environ.get('REMOTE_ADDR',
+ 'unidentified origin'))
+
+ # Was an attempt made to subscribe the list to itself?
+ if email == mlist.GetListEmail():
+ syslog('mischief', 'Attempt to self subscribe %s: %s', email, remote)
+ results.append(_('You may not subscribe a list to itself!'))
+
+ # If the user did not supply a password, generate one for him
+ password = cgidata.getvalue('pw')
+ confirmed = cgidata.getvalue('pw-conf')
+
+ if password is None and confirmed is None:
+ password = Utils.MakeRandomPassword()
+ elif password is None or confirmed is None:
+ results.append(_('If you supply a password, you must confirm it.'))
+ elif password <> confirmed:
+ results.append(_('Your passwords did not match.'))
+
+ # Get the digest option for the subscription.
+ digestflag = cgidata.getvalue('digest')
+ if digestflag:
+ try:
+ digest = int(digestflag)
+ except ValueError:
+ digest = 0
+ else:
+ digest = mlist.digest_is_default
+
+ # Sanity check based on list configuration. BAW: It's actually bogus that
+ # the page allows you to set the digest flag if you don't really get the
+ # choice. :/
+ if not mlist.digestable:
+ digest = 0
+ elif not mlist.nondigestable:
+ digest = 1
+
+ if results:
+ print_results(mlist, ERRORSEP.join(results), doc, lang)
+ return
+
+ # If this list has private rosters, we have to be careful about the
+ # message that gets printed, otherwise the subscription process can be
+ # used to mine for list members. It may be inefficient, but it's still
+ # possible, and that kind of defeats the purpose of private rosters.
+ # We'll use this string for all successful or unsuccessful subscription
+ # results.
+ if mlist.private_roster == 0:
+ # Public rosters
+ privacy_results = ''
+ else:
+ privacy_results = _("""\
+Your subscription request has been received, and will soon be acted upon.
+Depending on the configuration of this mailing list, your subscription request
+may have to be first confirmed by you via email, or approved by the list
+moderator. If confirmation is required, you will soon get a confirmation
+email which contains further instructions.""")
+
+ try:
+ userdesc = UserDesc(email, fullname, password, digest, lang)
+ mlist.AddMember(userdesc, remote)
+ results = ''
+ # Check for all the errors that mlist.AddMember can throw options on the
+ # web page for this cgi
+ except Errors.MembershipIsBanned:
+ results = _("""The email address you supplied is banned from this
+ mailing list. If you think this restriction is erroneous, please
+ contact the list owners at %(listowner)s.""")
+ except Errors.MMBadEmailError:
+ results = _("""\
+The email address you supplied is not valid. (E.g. it must contain an
+`@'.)""")
+ except Errors.MMHostileAddress:
+ results = _("""\
+Your subscription is not allowed because the email address you gave is
+insecure.""")
+ except Errors.MMSubscribeNeedsConfirmation:
+ # Results string depends on whether we have private rosters or not
+ if privacy_results:
+ results = privacy_results
+ else:
+ results = _("""\
+Confirmation from your email address is required, to prevent anyone from
+subscribing you without permission. Instructions are being sent to you at
+%(email)s. Please note your subscription will not start until you confirm
+your subscription.""")
+ except Errors.MMNeedApproval, x:
+ # Results string depends on whether we have private rosters or not
+ if privacy_results:
+ results = privacy_results
+ else:
+ # We need to interpolate into x
+ x = _(x)
+ results = _("""\
+Your subscription request was deferred because %(x)s. Your request has been
+forwarded to the list moderator. You will receive email informing you of the
+moderator's decision when they get to your request.""")
+ except Errors.MMAlreadyAMember:
+ # Results string depends on whether we have private rosters or not
+ if not privacy_results:
+ results = _('You are already subscribed.')
+ else:
+ results = privacy_results
+ # This could be a membership probe. For safety, let the user know
+ # a probe occurred. BAW: should we inform the list moderator?
+ listaddr = mlist.GetListEmail()
+ # Set the language for this email message to the member's language.
+ mlang = mlist.getMemberLanguage(email)
+ otrans = i18n.get_translation()
+ i18n.set_language(mlang)
+ try:
+ msg = Message.UserNotification(
+ mlist.getMemberCPAddress(email),
+ mlist.GetBouncesEmail(),
+ _('Mailman privacy alert'),
+ _("""\
+An attempt was made to subscribe your address to the mailing list
+%(listaddr)s. You are already subscribed to this mailing list.
+
+Note that the list membership is not public, so it is possible that a bad
+person was trying to probe the list for its membership. This would be a
+privacy violation if we let them do this, but we didn't.
+
+If you submitted the subscription request and forgot that you were already
+subscribed to the list, then you can ignore this message. If you suspect that
+an attempt is being made to covertly discover whether you are a member of this
+list, and you are worried about your privacy, then feel free to send a message
+to the list administrator at %(listowner)s.
+"""), lang=mlang)
+ finally:
+ i18n.set_translation(otrans)
+ msg.send(mlist)
+ # These shouldn't happen unless someone's tampering with the form
+ except Errors.MMCantDigestError:
+ results = _('This list does not support digest delivery.')
+ except Errors.MMMustDigestError:
+ results = _('This list only supports digest delivery.')
+ else:
+ # Everything's cool. Our return string actually depends on whether
+ # this list has private rosters or not
+ if privacy_results:
+ results = privacy_results
+ else:
+ results = _("""\
+You have been successfully subscribed to the %(realname)s mailing list.""")
+ # Show the results
+ print_results(mlist, results, doc, lang)
+
+
+
+def print_results(mlist, results, doc, lang):
+ # The bulk of the document will come from the options.html template, which
+ # includes its own html armor (head tags, etc.). Suppress the head that
+ # Document() derived pages get automatically.
+ doc.suppress_head = 1
+
+ replacements = mlist.GetStandardReplacements(lang)
+ replacements['<mm-results>'] = results
+ output = mlist.ParseTags('subscribe.html', replacements, lang)
+ doc.AddItem(output)
+ print doc.Format()