diff options
Diffstat (limited to 'mailman/web')
32 files changed, 0 insertions, 9725 deletions
diff --git a/mailman/web/Cgi/Auth.py b/mailman/web/Cgi/Auth.py deleted file mode 100644 index 825d972f4..000000000 --- a/mailman/web/Cgi/Auth.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Common routines for logging in and logging out of the list administrator -and list moderator interface. -""" - -from Mailman import Utils -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=False): - url = mlist.GetScriptURL(scriptname) - if frontpage: - actionurl = url - else: - request = Utils.GetRequestURI(url).lstrip('/') - up = '../' * request.count('/') - actionurl = up + request - 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).encode(charset) - print mlist.GetMailmanFooter().encode(charset) diff --git a/mailman/web/Cgi/__init__.py b/mailman/web/Cgi/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/mailman/web/Cgi/__init__.py +++ /dev/null diff --git a/mailman/web/Cgi/admin.py b/mailman/web/Cgi/admin.py deleted file mode 100644 index e5c6ee14b..000000000 --- a/mailman/web/Cgi/admin.py +++ /dev/null @@ -1,1433 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Process and produce the list-administration options forms.""" - -import os -import re -import cgi -import sha -import sys -import urllib -import logging - -from email.Utils import unquote, parseaddr, formataddr -from string import lowercase, digits - -from Mailman import Errors -from Mailman import MailList -from Mailman import MemberAdaptor -from Mailman import Utils -from Mailman import i18n -from Mailman import passwords -from Mailman.Cgi import Auth -from Mailman.UserDesc import UserDesc -from Mailman.configuration import config -from Mailman.htmlformat import * - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -NL = '\n' -OPTCOLUMNS = 11 - -log = logging.getLogger('mailman.error') - - - -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=False) - except Errors.MMListError, e: - # Avoid cross-site scripting attacks - safelistname = Utils.websafe(listname) - admin_overview(_('No such list <em>%(safelistname)s</em>')) - log.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((config.AuthListAdmin, - config.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(config.AuthListAdmin) - Auth.loginpage(mlist, 'admin', frontpage=True) - 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, list): - varhelp = qs[0] - if varhelp: - option_help(mlist, varhelp) - return - - # The html page document - doc = Document() - doc.set_language(mlist.preferred_language) - mlist.Lock() - try: - 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: - 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_request_domain() - legend = _('%(hostname)s mailing lists - Admin Links') - # The html `document' - doc = Document() - doc.set_language(config.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=config.WEB_HEADER_COLOR) - # Skip any mailing list that isn't advertised. - advertised = [] - for name in sorted(config.list_manager.names): - mlist = MailList.MailList(name, lock=False) - if mlist.advertised: - if hostname not in mlist.web_page_url: - # This list is situated in a different virtual domain - continue - else: - advertised.append((mlist.GetScriptURL('admin'), - mlist.real_name, - mlist.description)) - # 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(config.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_noreply() - 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([' ', ' ']) - table.AddRow([Bold(FontAttr(_('List'), size='+2')), - Bold(FontAttr(_('Description'), size='+2')) - ]) - highlight = 1 - for url, real_name, description in advertised: - table.AddRow( - [Link(url, Bold(real_name)), - description or Italic(_('[no description available]'))]) - if highlight and config.WEB_HIGHLIGHT_COLOR: - table.AddRowInfo(table.GetCurrentRowIndex(), - bgcolor=config.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) - if options: - 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=config.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 and text files'))) - otherlinks.AddItem(Link(mlist.GetBaseArchiveURL(), - _('Go to list archives')).Format() + - '<br> <br>') - if config.OWNERS_CAN_DELETE_THEIR_OWN_LISTS: - otherlinks.AddItem(Link(mlist.GetScriptURL('rmlist'), - _('Delete this mailing list')).Format() + - _(' (requires confirmation)<br> <br>')) - otherlinks.AddItem(Link('%s/logout' % adminurl, - # BAW: What I really want is a blank line, but - # adding an 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 = config.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=config.WEB_HEADER_COLOR) - # Add a blank separator row - table.AddRow([' ', ' ']) - # 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=config.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, basestring): - 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 isinstance(item, basestring): - # 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=config.WEB_ADMINITEM_COLOR) - table.AddCellInfo(table.GetCurrentRowIndex(), 1, - bgcolor=config.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 == config.Radio or kind == config.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 config.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 == config.String or kind == config.Email or - kind == config.Host or kind == config.Number): - return TextBox(varname, value, params) - elif kind == config.Text: - if params: - r, c = params - else: - r, c = None, None - return TextArea(varname, value or '', r, c) - elif kind in (config.EmailList, config.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 == config.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 == config.Select: - if params: - values, legend, selected = params - else: - codes = mlist.language_codes - legend = [config.languages.get_description(code) for code in codes] - selected = codes.index(mlist.preferred_language) - return SelectOptions(varname, values, legend, selected) - elif kind == config.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=False, 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=True) - return table - elif kind == config.HeaderFilter: - # A complex and specialized widget type that allows for setting of a - # spam filter rule including, a mark button, a regexp text box, an - # "add after mark", up and down buttons, and a delete button. Yeesh! - # params are ignored. - table = Table(border=0) - # This adds the html for the entry widget - def makebox(i, pattern, action, empty=False, table=table): - deltag = 'hdrfilter_delete_%02d' % i - reboxtag = 'hdrfilter_rebox_%02d' % i - actiontag = 'hdrfilter_action_%02d' % i - wheretag = 'hdrfilter_where_%02d' % i - addtag = 'hdrfilter_add_%02d' % i - newtag = 'hdrfilter_new_%02d' % i - uptag = 'hdrfilter_up_%02d' % i - downtag = 'hdrfilter_down_%02d' % i - if empty: - table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))), - Hidden(newtag)]) - else: - table.AddRow([Center(Bold(_('Spam Filter Rule %(i)d'))), - SubmitButton(deltag, _('Delete'))]) - table.AddRow([Label(_('Spam Filter Regexp:')), - TextArea(reboxtag, text=pattern, - rows=4, cols=30, wrap='off')]) - values = [config.DEFER, config.HOLD, config.REJECT, - config.DISCARD, config.ACCEPT] - try: - checked = values.index(action) - except ValueError: - checked = 0 - radio = RadioButtonArray( - actiontag, - (_('Defer'), _('Hold'), _('Reject'), - _('Discard'), _('Accept')), - values=values, - checked=checked).Format() - table.AddRow([Label(_('Action:')), radio]) - if not empty: - table.AddRow([SubmitButton(addtag, _('Add new item...')), - SelectOptions(wheretag, ('before', 'after'), - (_('...before this one.'), - _('...after this one.')), - selected=1), - ]) - # BAW: IWBNI we could disable the up and down buttons for the - # first and last item respectively, but it's not easy to know - # which is the last item, so let's not worry about that for - # now. - table.AddRow([SubmitButton(uptag, _('Move rule up')), - SubmitButton(downtag, _('Move rule down'))]) - 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 pattern, action, empty in data: - makebox(i, pattern, action, 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, '', config.DEFER, empty=True) - return table - elif kind == config.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') - 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=config.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=config.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=config.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 - # The email addresses had /better/ be ASCII, but might be encoded in the - # database as Unicodes. - all = [_m.encode() for _m in 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=config.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 = ' '*2 + '\n' - usertable.AddRow([Center(joiner.join(cells))]) - usertable.AddCellInfo(usertable.GetCurrentRowIndex(), - usertable.GetCurrentCellIndex(), - colspan=OPTCOLUMNS, - bgcolor=config.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=config.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 = mlist.getMemberName(addr) - 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, config.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, config.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, config.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.language_codes - langdescs = [_(config.languges.get_description(code)) - for code 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, list): - 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 = config.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([' ', ' ']) - 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 = config.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') - table = Table(cellspacing=3, cellpadding=4) - table.AddRow([Center(Header(2, _('Change list ownership passwords')))]) - table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=2, - bgcolor=config.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=config.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=config.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 = passwords.make_secret( - new, config.PASSWORD_SCHEME) - # 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 = passwords.make_secret(new, config.PASSWORD_SCHEME) - # Set new cookie - print mlist.MakeCookie(config.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 = mlist.digest_is_default - 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, - whence='admin mass sub') - except Errors.MMAlreadyAMember: - subscribe_errors.append((entry, _('Already a member'))) - except Errors.InvalidEmailAddress: - if userdesc.address == '': - subscribe_errors.append((_('<blank line>'), - _('Bad/Invalid email address'))) - else: - subscribe_errors.append((entry, - _('Bad/Invalid email address'))) - except Errors.MembershipIsBanned, pattern: - subscribe_errors.append( - (entry, _('Banned address (matched %(pattern)s)'))) - 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, config.Moderate, val) - # do the user options for members category - if cgidata.has_key('setmemberopts_btn') and cgidata.has_key('user'): - user = cgidata['user'] - if isinstance(user, list): - 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, whence='member mgt page') - 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, config.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 not in config.languages.enabled_codes - and newlang <> oldlang): - # Then - mlist.setMemberLanguage(user, newlang) - - moderate = not not cgidata.getvalue(user+'_mod') - mlist.setMemberOption(user, config.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 = config.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/web/Cgi/admindb.py b/mailman/web/Cgi/admindb.py deleted file mode 100644 index 5d756fc78..000000000 --- a/mailman/web/Cgi/admindb.py +++ /dev/null @@ -1,813 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Produce and process the pending-approval items for a list.""" - -import os -import cgi -import sys -import time -import email -import errno -import logging - -from urllib import quote_plus, unquote_plus - -from Mailman import Errors -from Mailman import MailList -from Mailman import Message -from Mailman import Utils -from Mailman import i18n -from Mailman.Cgi import Auth -from Mailman.Handlers.Moderate import ModeratedMemberPost -from Mailman.ListAdmin import readMessage -from Mailman.configuration import config -from Mailman.htmlformat import * -from Mailman.interfaces import RequestType - -EMPTYSTRING = '' -NL = '\n' - -# Set up i18n. Until we know which list is being requested, we use the -# server's default. -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -EXCERPT_HEIGHT = 10 -EXCERPT_WIDTH = 76 - -log = logging.getLogger('mailman.error') - - - -def helds_by_sender(mlist): - bysender = {} - requests = config.db.get_list_requests(mlist) - for request in requests.of_type(RequestType.held_message): - key, data = requests.get_request(request.id) - sender = data.get('sender') - assert sender is not None, ( - 'No sender for held message: %s' % request.id) - bysender.setdefault(sender, []).append(request.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 = ' ' * 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>')) - log.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((config.AuthListAdmin, - config.AuthListModerator, - config.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 isinstance(qs, list): - sender = qs[0] - qs = cgi.parse_qs(envar).get('msgid') - if qs and isinstance(qs, list): - msgid = qs[0] - qs = cgi.parse_qs(envar).get('details') - if qs and isinstance(qs, list): - details = qs[0] - - mlist.Lock() - try: - realname = mlist.real_name - if not cgidata.keys() or cgidata.has_key('admlogin'): - # If this is not a form submission (i.e. there are no keys in the - # form) or it's a login, then 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 config.db.requests.get_list_requests(mlist).count == 0: - 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'), - _('Click here to reload this page.'))) - doc.AddItem(mlist.GetMailmanFooter()) - print doc.Format() - mlist.Save() - return - - admindburl = mlist.GetScriptURL('admindb') - form = Form(admindburl) - # Add the instructions template - if details == 'instructions': - doc.AddItem(Header( - 2, _('Detailed instructions for the administrative database'))) - else: - doc.AddItem(Header( - 2, - _('Administrative requests for mailing list:') - + ' <em>%s</em>' % mlist.real_name)) - if details <> 'instructions': - form.AddItem(Center(SubmitButton('submit', _('Submit All Data')))) - requestsdb = config.db.get_list_requests(mlist) - message_count = requestsdb.count_of(RequestType.held_message) - if not (details or sender or msgid or message_count == 0): - form.AddItem(Center( - CheckBox('discardalldefersp', 0).Format() + - ' ' + - _('Discard all messages marked <em>Defer</em>') - )) - # Add a link back to the overview, if we're not viewing the overview! - adminurl = mlist.GetScriptURL('admin') - 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>') - if not (details or sender or msgid or nomessages): - form.AddItem(Center( - CheckBox('discardalldefersp', 0).Format() + - ' ' + - _('Discard all messages marked <em>Defer</em>') - )) - form.AddItem(Center(SubmitButton('submit', _('Submit All Data')))) - doc.AddItem(mlist.GetMailmanFooter()) - print doc.Format() - # 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(config.DEFAULT_SERVER_LANGUAGE) - - header = _('Mailman Administrative Database Error') - doc.SetTitle(header) - doc.AddItem(Header(2, header)) - doc.AddItem(msg) - url = Utils.ScriptURL('admin') - 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 - requestsdb = config.db.get_list_requests(mlist) - if requestsdb.count_of(RequestType.subscription) == 0: - 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 request in requestsdb.of_type(RequestType.subscription): - key, data = requestsdb.get_request(requst.id) - addr = data['addr'] - byaddrs.setdefault(addr, []).append(request.id) - addrs = sorted(byaddrs) - num = 0 - for addr, ids in byaddrs.items(): - # Eliminate duplicates - for id in ids[1:]: - mlist.HandleRequest(id, config.DISCARD) - id = ids[0] - key, data = requestsdb.get_request(id) - time = data['time'] - addr = data['addr'] - fullname = data['fullname'] - passwd = data['passwd'] - digest = data['digest'] - lang = data['lang'] - fullname = Utils.uncanonstr(fullname, mlist.preferred_language) - radio = RadioButtonArray(id, (_('Defer'), - _('Approve'), - _('Reject'), - _('Discard')), - values=(config.DEFER, - config.SUBSCRIBE, - config.REJECT, - config.DISCARD), - checked=0).Format() - if addr not in mlist.ban_list: - radio += '<br>' + CheckBox('ban-%d' % id, 1).Format() + \ - ' ' + _('Permanently ban from this list') - # While the address may be a unicode, it must be ascii - paddr = addr.encode('us-ascii', 'replace') - table.AddRow(['%s<br><em>%s</em>' % (paddr, 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 - requestsdb = config.db.get_list_requests(mlist) - if requestsdb.count_of(RequestType.unsubscription) == 0: - 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 request in requestsdb.of_type(RequestType.unsubscription): - key, data = requestsdb.get_request(request.id) - addr = data['addr'] - byaddrs.setdefault(addr, []).append(request.id) - addrs = sorted(byaddrs) - num = 0 - for addr, ids in byaddrs.items(): - # Eliminate duplicates - for id in ids[1:]: - mlist.HandleRequest(id, config.DISCARD) - id = ids[0] - key, data = requestsdb.get_record(id) - addr = data['addr'] - 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, config.DISCARD) - continue - num += 1 - table.AddRow(['%s<br><em>%s</em>' % (addr, fullname), - RadioButtonArray(id, (_('Defer'), - _('Approve'), - _('Reject'), - _('Discard')), - values=(config.DEFER, - config.UNSUBSCRIBE, - config.REJECT, - config.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 - form.AddItem('<hr>') - form.AddItem(Center(Header(2, _('Held Messages')))) - # Add the by-sender overview tables - admindburl = mlist.GetScriptURL('admindb') - 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')), - (config.DEFER, config.APPROVE, config.REJECT, config.DISCARD), - (1, 0, 0, 0)) - left.AddRow([btns]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - left.AddRow([ - CheckBox('senderpreserve-' + qsender, 1).Format() + - ' ' + - _('Preserve messages for the site administrator') - ]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - left.AddRow([ - CheckBox('senderforward-' + qsender, 1).Format() + - ' ' + - _('Forward messages (individually) to:') - ]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - left.AddRow([ - TextBox('senderforwardto-' + qsender, - value=mlist.GetOwnerEmail()) - ]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - # If 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, config.Moderate): - left.AddRow([ - CheckBox('senderclearmodp-' + qsender, 1).Format() + - ' ' + - _("Clear this member's <em>moderate</em> flag") - ]) - else: - left.AddRow( - [_('<em>The sender is now a member of this list</em>')]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - elif sender not in (mlist.accept_these_nonmembers + - mlist.hold_these_nonmembers + - mlist.reject_these_nonmembers + - mlist.discard_these_nonmembers): - left.AddRow([ - CheckBox('senderfilterp-' + qsender, 1).Format() + - ' ' + - _('Add <b>%(esender)s</b> to one of these sender filters:') - ]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - btns = hacky_radio_buttons( - 'senderfilter-' + qsender, - (_('Accepts'), _('Holds'), _('Rejects'), _('Discards')), - (config.ACCEPT, config.HOLD, config.REJECT, config.DISCARD), - (0, 0, 0, 1)) - left.AddRow([btns]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - if sender not in mlist.ban_list: - left.AddRow([ - CheckBox('senderbanp-' + qsender, 1).Format() + - ' ' + - _("""Ban <b>%(esender)s</b> from ever subscribing to this - mailing list""")]) - left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) - right = Table(border=0) - right.AddRow([ - _("""Click on the message number to view the individual - message, or you can """) + - Link(senderurl, _('view all messages from %(esender)s')).Format() - ]) - right.AddCellInfo(right.GetCurrentRowIndex(), 0, colspan=2) - right.AddRow([' ', ' ']) - counter = 1 - for id in bysender[sender]: - key, data = requestsdb.get_record(id) - ptime = data['ptime'] - sender = data['sender'] - subject = data['subject'] - reason = data['reason'] - filename = data['filename'] - msgdata = data['msgdata'] - # 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(config.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, config.DISCARD) - continue - dispsubj = Utils.oneline( - subject, Utils.GetCharSet(mlist.preferred_language)) - t = Table(border=0) - t.AddRow([Link(admindburl + '?msgid=%d' % id, '[%d]' % counter), - Bold(_('Subject:')), - Utils.websafe(dispsubj) - ]) - t.AddRow([' ', Bold(_('Size:')), str(size) + _(' bytes')]) - if reason: - reason = _(reason) - else: - reason = _('not available') - t.AddRow([' ', Bold(_('Reason:')), reason]) - # Include the date we received the message, if available - when = msgdata.get('received_time') - if when: - t.AddRow([' ', Bold(_('Received:')), - time.ctime(when)]) - counter += 1 - right.AddRow([t]) - stable.AddRow([left, right]) - table.AddRow([stable]) - return 1 - - - -def show_sender_requests(mlist, form, sender): - bysender = helds_by_sender(mlist) - if not bysender: - return - sender_ids = bysender.get(sender) - if sender_ids is None: - # BAW: should we print an error message? - return - total = len(sender_ids) - requestsdb = config.db.get_list_requests(mlist) - for i, id in enumerate(sender_ids): - key, data = requestsdb.get_record(id) - show_post_requests(mlist, id, data, total, count + 1, form) - - - -def show_message_requests(mlist, form, id): - requestdb = config.db.get_list_requests(mlist) - try: - id = int(id) - info = requestdb.get_record(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): - requestsdb = config.db.get_list_requests(mlist) - total = requestsdb.count_of(RequestType.held_message) - all = requestsdb.of_type(RequestType.held_message) - for i, request in enumerate(all): - key, data = requestdb.get_request(request.id) - show_post_requests(mlist, request.id, data, total, i + 1, form) - - - -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(config.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, config.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, config.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 = config.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)[:config.ADMINDB_PAGE_TEXT_LIMIT] - else: - body = EMPTYSTRING.join(lines) - # Get message charset and try encode in list charset - mcset = msg.get_param('charset', 'us-ascii').lower() - lcset = Utils.GetCharSet(mlist.preferred_language) - if mcset <> lcset: - try: - body = unicode(body, mcset).encode(lcset) - except (LookupError, UnicodeError, ValueError): - pass - hdrtxt = NL.join(['%s: %s' % (k, v) for k, v in msg.items()]) - hdrtxt = Utils.websafe(hdrtxt) - # 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(Utils.oneline(subject, lcset))]) - t.AddCellInfo(row+1, col-1, align='right') - t.AddRow([Bold(_('Reason:')), _(reason)]) - t.AddCellInfo(row+2, col-1, align='right') - when = msgdata.get('received_time') - if when: - t.AddRow([Bold(_('Received:')), time.ctime(when)]) - t.AddCellInfo(row+2, col-1, align='right') - # 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=' '*5: s+x+s, - (_('Defer'), _('Approve'), _('Reject'), _('Discard')))) - buttons.AddRow([Center(RadioButton(id, config.DEFER, 1)), - Center(RadioButton(id, config.APPROVE, 0)), - Center(RadioButton(id, config.REJECT, 0)), - Center(RadioButton(id, config.DISCARD, 0)), - ]) - t.AddRow([Bold(_('Action:')), buttons]) - t.AddCellInfo(row+3, col-1, align='right') - t.AddRow([' ', - CheckBox('preserve-%d' % id, 'on', 0).Format() + - ' ' + _('Preserve message for site administrator') - ]) - t.AddRow([' ', - CheckBox('forward-%d' % id, 'on', 0).Format() + - ' ' + _('Additionally, forward this message to: ') + - TextBox('forward-addr-%d' % id, size=47, - value=mlist.GetOwnerEmail()).Format() - ]) - notice = msgdata.get('rejection_notice', _('[No explanation given]')) - t.AddRow([ - Bold(_('If you reject this post,<br>please explain (optional):')), - TextArea('comment-%d' % id, rows=4, cols=EXCERPT_WIDTH, - text = Utils.wrap(_(notice), column=80)) - ]) - row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex() - t.AddCellInfo(row, col-1, align='right') - t.AddRow([Bold(_('Message Headers:')), - TextArea('headers-%d' % id, hdrtxt, - rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)]) - row, col = t.GetCurrentRowIndex(), t.GetCurrentCellIndex() - t.AddCellInfo(row, col-1, align='right') - t.AddRow([Bold(_('Message Excerpt:')), - TextArea('fulltext-%d' % id, Utils.websafe(body), - rows=EXCERPT_HEIGHT, cols=EXCERPT_WIDTH, readonly=1)]) - t.AddCellInfo(row+1, col-1, align='right') - form.AddItem(t) - form.AddItem('<p>') - - - -def process_form(mlist, doc, cgidata): - senderactions = {} - # 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 - # discard-all-defers - try: - discardalldefersp = cgidata.getvalue('discardalldefersp', 0) - except ValueError: - discardalldefersp = 0 - 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', config.DEFER)) - except ValueError: - action = config.DEFER - if action == config.DEFER and discardalldefersp: - action = config.DISCARD - if action in (config.DEFER, config.APPROVE, - config.REJECT, config.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 == config.ACCEPT: - mlist.accept_these_nonmembers.append(sender) - elif which == config.HOLD: - mlist.hold_these_nonmembers.append(sender) - elif which == config.REJECT: - mlist.reject_these_nonmembers.append(sender) - elif which == config.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, config.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 - banaddrs = [] - erroraddrs = [] - for k in cgidata.keys(): - formv = cgidata[k] - if isinstance(formv, list): - continue - try: - v = int(formv.value) - request_id = int(k) - except ValueError: - continue - if v not in (config.DEFER, config.APPROVE, config.REJECT, - config.DISCARD, config.SUBSCRIBE, config.UNSUBSCRIBE, - config.ACCEPT, config.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. - requestsdb = config.db.get_list_requests(mlist) - if cgidata.getvalue(bankey): - key, data = requestsdb.get_record(request_id) - sender = data['sender'] - 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) - except Errors.MembershipIsBanned, pattern: - data = requestsdb.get_record(request_id) - sender = data['sender'] - banaddrs.append((sender, pattern)) - # 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>') - if banaddrs: - for addr, patt in banaddrs: - doc.AddItem(_('%(addr)s is banned (matched: %(patt)s)') + '<br>') diff --git a/mailman/web/Cgi/confirm.py b/mailman/web/Cgi/confirm.py deleted file mode 100644 index 188e43068..000000000 --- a/mailman/web/Cgi/confirm.py +++ /dev/null @@ -1,834 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Confirm a pending action via URL.""" - -import cgi -import time -import signal -import logging - -from Mailman import Errors -from Mailman import MailList -from Mailman import Pending -from Mailman import i18n -from Mailman.UserDesc import UserDesc -from Mailman.configuration import config -from Mailman.htmlformat import * - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -log = logging.getLogger('mailman.error') - - - -def main(): - doc = Document() - doc.set_language(config.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() - log.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(config.PENDING_REQUEST_LIFE / config.days(1) + 0.5) - confirmurl = mlist.GetScriptURL('confirm') - # 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 = mlist.pend_confirm(cookie, expunge=False) - 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?""")) - # Expunge this record from the pending database. - expunge(mlist, 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.""")) - # Expunge this record from the pending database. - expunge(mlist, cookie) - 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 expunge(mlist, cookie): - # Expunge this record from the list's pending database. This requires - # that the list lock be acquired, however the list doesn't need to be - # saved because this operation doesn't touch the config.pck file. - mlist.Lock() - try: - mlist.pend_confirm(cookie, expunge=True) - finally: - mlist.Unlock() - - - -def ask_for_cookie(mlist, doc, extra=''): - title = _('Enter confirmation cookie') - doc.SetTitle(title) - form = Form(mlist.GetScriptURL('confirm')) - table = Table(border=0, width='100%') - table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) - table.AddCellInfo(table.GetCurrentRowIndex(), 0, - colspan=2, bgcolor=config.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')) - table = Table(border=0, width='100%') - table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) - table.AddCellInfo(table.GetCurrentRowIndex(), 0, - colspan=2, bgcolor=config.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 my subscription request</em> if you no longer want to - subscribe to this list.""") + '<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.language_codes - values = [_(config.languages.get_description(code)) for code 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): - mlist.Lock() - try: - # Discard this cookie - userdesc = mlist.pend_confirm(cookie)[1] - finally: - mlist.Unlock() - 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') - if lang not in config.languages.enabled_codes: - lang = mlist.preferred_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 = mlist.pend_confirm(cookie, expunge=False)[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!")) - except Errors.MembershipIsBanned: - owneraddr = mlist.GetOwnerEmail() - doc.addError(_("""You are currently banned from subscribing to - this list. If you think this restriction is erroneous, please - contact the list owners at %(owneraddr)s.""")) - except Errors.HostileSubscriptionError: - doc.addError(_("""\ - You were not invited to this mailing list. The invitation has - been discarded, and both list administrators have been - alerted.""")) - 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) - 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): - # Expunge this record from the pending database - expunge(mlist, cookie) - 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 = mlist.pend_confirm(cookie, expunge=False) - 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') - 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')) - table = Table(border=0, width='100%') - table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) - table.AddCellInfo(table.GetCurrentRowIndex(), 0, - colspan=2, bgcolor=config.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): - # Expunge this record from the pending database - expunge(mlist, cookie) - 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 = mlist.pend_confirm( - cookie, expunge=False) - 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.''')) - except Errors.MembershipIsBanned: - owneraddr = mlist.GetOwnerEmail() - realname = mlist.real_name - doc.addError(_("""%(newaddr)s is banned from subscribing to the - %(realname)s list. If you think this restriction is erroneous, - please contact the list owners at %(owneraddr)s.""")) - else: - # The response - listname = mlist.real_name - title = _('Change of address request confirmed') - optionsurl = mlist.GetOptionsURL(newaddr) - 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')) - table = Table(border=0, width='100%') - table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) - table.AddCellInfo(table.GetCurrentRowIndex(), 0, - colspan=2, bgcolor=config.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): - 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=config.WEB_HEADER_COLOR) - # Expunge this record from the pending database. - expunge(mlist, cookie) - 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 = mlist.pend_confirm(cookie) - requestsdb = config.db.get_list_requests(mlist) - key, data = requestsdb.get_record(id) - sender = data['sender'] - msgsubject = data['msgsubject'] - subject = Utils.websafe(msgsubject) - lang = mlist.getMemberLanguage(sender) - i18n.set_language(lang) - doc.set_language(lang) - # Discard the message - mlist.HandleRequest(id, config.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')) - table = Table(border=0, width='100%') - table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) - table.AddCellInfo(table.GetCurrentRowIndex(), 0, - colspan=2, bgcolor=config.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() - requestdb = config.db.get_list_requests(mlist) - try: - try: - key, data = requestdb.get_record(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 = mlist.pend_confirm(cookie, expunge=False) - 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) - 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')) - table = Table(border=0, width='100%') - table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) - table.AddCellInfo(table.GetCurrentRowIndex(), 0, - colspan=2, bgcolor=config.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') - # 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', - time.localtime(time.mktime(info.date + (0,)*6))) - daysleft = int(info.noticesleft * - mlist.bounce_you_are_disabled_warnings_interval / - config.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/web/Cgi/create.py b/mailman/web/Cgi/create.py deleted file mode 100644 index 4bb650957..000000000 --- a/mailman/web/Cgi/create.py +++ /dev/null @@ -1,400 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Create mailing lists through the web.""" - -import cgi -import sha -import sys -import logging - -from Mailman import Errors -from Mailman import MailList -from Mailman import Message -from Mailman import i18n -from Mailman import passwords -from Mailman.configuration import config -from Mailman.htmlformat import * - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) -__i18n_templates__ = True - -log = logging.getLogger('mailman.error') - - - -def main(): - doc = Document() - doc.set_language(config.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')))) - log.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 = bool(int(cgidata.getvalue('autogen', '0'))) - except ValueError: - autogen = False - try: - notify = bool(int(cgidata.getvalue('notify', '0'))) - except ValueError: - notify = False - try: - moderate = bool(int(cgidata.getvalue('moderate', - config.DEFAULT_DEFAULT_MEMBER_MODERATION))) - except ValueError: - moderate = config.DEFAULT_DEFAULT_MEMBER_MODERATION - - password = cgidata.getvalue('password', '').strip() - confirm = cgidata.getvalue('confirm', '').strip() - auth = cgidata.getvalue('auth', '').strip() - langs = cgidata.getvalue('langs', [config.DEFAULT_SERVER_LANGUAGE]) - - if not isinstance(langs, list): - langs = [langs] - # Sanity checks - safelistname = Utils.websafe(listname) - if '@' in listname: - request_creation(doc, cgidata, - _('List name must not include "@": $safelistname')) - 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( - config.ADMIN_PASSWORD_LENGTH) - 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 = False - if auth: - ok = Utils.check_global_password(auth, False) - 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 - # Make sure the url host name matches one of our virtual domains. Then - # calculate the list's posting address. - url_host = Utils.get_request_domain() - # Find the IDomain matching this url_host if there is one. - for email_host, domain in config.domains: - if domain.url_host == url_host: - email_host = domain.email_host - break - else: - safehostname = Utils.websafe(url_host) - request_creation(doc, cgidata, - _('Unknown virtual host: $safehostname')) - return - fqdn_listname = '%s@%s' % (listname, email_host) - # We've got all the data we need, so go ahead and try to create the list - mlist = MailList.MailList() - try: - scheme = passwords.lookup_scheme(config.PASSWORD_SCHEME.lower()) - pw = passwords.make_secret(password, scheme) - try: - mlist.Create(fqdn_listname, owner, pw, langs) - except Errors.EmailAddressError, s: - request_creation(doc, cgidata, _('Bad owner email address: $s')) - return - except Errors.MMListAlreadyExistsError: - safelistname = Utils.websafe(listname) - request_creation(doc, cgidata, - _('List already exists: $safelistname')) - return - except Errors.InvalidEmailAddress, s: - request_creation(doc, cgidata, _('Illegal list name: $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. - mlist.default_member_moderation = moderate - mlist.Save() - finally: - mlist.Unlock() - # Now do the MTA-specific list creation tasks - if config.MTA: - modname = 'Mailman.MTA.' + config.MTA - __import__(modname) - sys.modules[modname].create(mlist, cgi=True) - # And send the notice to the list owner. - if notify: - text = Utils.maketext( - 'newlist.txt', - {'listname' : listname, - 'password' : password, - 'admin_url' : mlist.GetScriptURL('admin', absolute=True), - 'listinfo_url': mlist.GetScriptURL('listinfo', absolute=True), - 'requestaddr' : mlist.GetRequestEmail(), - 'siteowner' : mlist.no_reply_address, - }, mlist=mlist) - msg = Message.UserNotification( - owner, mlist.no_reply_address, - _('Your new mailing list: $listname'), - text, mlist.preferred_language) - msg.send(mlist) - # Success! - listinfo_url = mlist.GetScriptURL('listinfo') - admin_url = mlist.GetScriptURL('admin') - 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=config.WEB_HEADER_COLOR) - table.AddRow([_("""You have successfully created the mailing list - <b>$listname</b> and notification has been sent to the list owner - <b>$owner</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_request_domain() - # Set up the document - title = _('Create a $hostname 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=config.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 = config.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) - - safelistname = Utils.websafe(cgidata.getvalue('listname', '')) - ftable.AddRow([Label(_('Name of list:')), - TextBox('listname', safelistname)]) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY) - - safeowner = Utils.websafe(cgidata.getvalue('owner', '')) - ftable.AddRow([Label(_('Initial list owner address:')), - TextBox('owner', safeowner)]) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY) - - try: - autogen = bool(int(cgidata.getvalue('autogen', '0'))) - except ValueError: - autogen = False - 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) - - safepasswd = Utils.websafe(cgidata.getvalue('password', '')) - ftable.AddRow([Label(_('Initial list password:')), - PasswordBox('password', safepasswd)]) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY) - - safeconfirm = Utils.websafe(cgidata.getvalue('confirm', '')) - ftable.AddRow([Label(_('Confirm initial password:')), - PasswordBox('confirm', safeconfirm)]) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 0, bgcolor=GREY) - ftable.AddCellInfo(ftable.GetCurrentRowIndex(), 1, bgcolor=GREY) - - try: - notify = bool(int(cgidata.getvalue('notify', '1'))) - except ValueError: - notify = True - try: - moderate = bool(int(cgidata.getvalue('moderate', - config.DEFAULT_DEFAULT_MEMBER_MODERATION))) - except ValueError: - moderate = config.DEFAULT_DEFAULT_MEMBER_MODERATION - - 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=moderate, - 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 config.LC_DESCRIPTIONS.items(): - revmap[_(name)] = key - langnames = revmap.keys() - langnames.sort() - langs = [] - for name in langnames: - langs.append(revmap[name]) - try: - langi = langs.index(config.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 = _( - config.languages.get_description(config.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""")), - CheckBoxArray('langs', - [_(config.languges.get_description(code)) - for code 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/web/Cgi/edithtml.py b/mailman/web/Cgi/edithtml.py deleted file mode 100644 index dfc871ec1..000000000 --- a/mailman/web/Cgi/edithtml.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Script which implements admin editing of the list's html templates.""" - -import os -import re -import cgi -import errno -import logging - -from Mailman import Defaults -from Mailman import Errors -from Mailman import MailList -from Mailman import Utils -from Mailman import i18n -from Mailman.Cgi import Auth -from Mailman.HTMLFormatter import HTMLFormatter -from Mailman.configuration import config -from Mailman.htmlformat import * - -_ = i18n._ - -log = logging.getLogger('mailman.error') - - - -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')), - ('subscribeack.txt', _('Welcome email text file')), - ) - - _ = i18n._ - doc = Document() - - # Set up the system default language - i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - doc.set_language(config.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() - log.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((Defaults.AuthListAdmin, - Defaults.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 - code = re.sub(r'<([/]?script.*?)>', r'<\1>', code) - langdir = os.path.join(mlist.fullpath(), mlist.preferred_language) - # Make sure the directory exists - Utils.makedirs(langdir) - 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/web/Cgi/listinfo.py b/mailman/web/Cgi/listinfo.py deleted file mode 100644 index 149be715f..000000000 --- a/mailman/web/Cgi/listinfo.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""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 -import logging - -from Mailman import Errors -from Mailman import MailList -from Mailman import Utils -from Mailman import i18n -from Mailman.configuration import config -from Mailman.htmlformat import * - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -log = logging.getLogger('mailman.error') - - - -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>')) - log.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') - if language not in config.languages.enabled_codes: - language = mlist.preferred_language - i18n.set_language(language) - list_listinfo(mlist, language) - - - -def listinfo_overview(msg=''): - # Present the general listinfo overview - hostname = Utils.get_request_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(config.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=config.WEB_HEADER_COLOR) - - # Skip any mailing lists that isn't advertised. - advertised = [] - for name in sorted(config.list_manager.names): - mlist = MailList.MailList(name, lock=False) - if mlist.advertised: - if hostname not in mlist.web_page_url: - # This list is situated in a different virtual domain - continue - else: - advertised.append((mlist.GetScriptURL('listinfo'), - mlist.real_name, - mlist.description)) - if msg: - greeting = FontAttr(msg, color="ff5060", size="+1") - else: - greeting = FontAttr(_('Welcome!'), size='+2') - - welcome = [greeting] - mailmanlink = Link(config.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_noreply() - 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>If you are having trouble using the lists, please contact '''), - Link('mailto:' + siteowner, siteowner), - '.<p>')) - - table.AddRow([apply(Container, welcome)]) - table.AddCellInfo(max(table.GetCurrentRowIndex(), 0), 0, colspan=2) - - if advertised: - table.AddRow([' ', ' ']) - table.AddRow([Bold(FontAttr(_('List'), size='+2')), - Bold(FontAttr(_('Description'), size='+2')) - ]) - highlight = 1 - for url, real_name, description in advertised: - table.AddRow( - [Link(url, Bold(real_name)), - description or Italic(_('[no description available]'))]) - if highlight and config.WEB_HIGHLIGHT_COLOR: - table.AddRowInfo(table.GetCurrentRowIndex(), - bgcolor=config.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.language_codes) == 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/web/Cgi/options.py b/mailman/web/Cgi/options.py deleted file mode 100644 index c529a3ea4..000000000 --- a/mailman/web/Cgi/options.py +++ /dev/null @@ -1,1000 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Produce and handle the member options.""" - -import os -import cgi -import sys -import urllib -import logging - -from Mailman import Errors -from Mailman import MailList -from Mailman import MemberAdaptor -from Mailman import Utils -from Mailman import i18n -from Mailman import passwords -from Mailman.configuration import config -from Mailman.htmlformat import * - - -OR = '|' -SLASH = '/' -SETLANGUAGE = -1 - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -log = logging.getLogger('mailman.error') -mlog = logging.getLogger('mailman.mischief') - - - -def main(): - doc = Document() - doc.set_language(config.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() - log.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') - if language not in config.languages.enabled_codes: - 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, language) - print doc.Format() - return - else: - user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:]))) - - # Avoid cross-site scripting attacks - safeuser = Utils.websafe(user) - try: - Utils.ValidateEmail(user) - except Errors.EmailAddressError: - doc.addError(_('Illegal Email Address: %(safeuser)s')) - loginpage(mlist, doc, None, language) - print doc.Format() - return - # Sanity check the user, but only give the "no such member" error when - # using public rosters, otherwise, we'll leak membership information. - if not mlist.isMember(user) and mlist.private_roster == 0: - doc.addError(_('No such member: %(safeuser)s.')) - loginpage(mlist, doc, None, language) - 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') - if userlang not in config.languages.enabled_codes: - userlang = 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 isinstance(qs, list): - 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): - # We must acquire the list lock in order to pend a request. - try: - mlist.Lock() - # If unsubs require admin approval, then this request has to - # be held. Otherwise, send a confirmation. - if mlist.unsubscribe_policy: - mlist.HoldUnsubscription(user) - doc.addError(_("""Your unsubscription request has been - forwarded to the list administrator for approval."""), - tag='') - else: - mlist.ConfirmUnsubscription(user, userlang) - doc.addError(_('The confirmation email has been sent.'), - tag='') - mlist.Save() - finally: - mlist.Unlock() - else: - # Not a member - if mlist.private_roster == 0: - # Public rosters - doc.addError(_('No such member: %(safeuser)s.')) - else: - mlog.error('Unsub attempt of non-member w/ private rosters: %s', - user) - doc.addError(_('The confirmation email has been sent.'), - tag='') - loginpage(mlist, doc, user, language) - 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: - mlog.error( - '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, language) - print doc.Format() - return - - # Get the password from the form. - password = cgidata.getvalue('password', '').strip() - # Check authentication. We need to know if the credentials match the user - # or the site admin, because they are the only ones who are allowed to - # change things globally. Specifically, the list admin may not change - # values globally. - if config.ALLOW_SITE_ADMIN_COOKIES: - user_or_siteadmin_context = (config.AuthUser, config.AuthSiteAdmin) - else: - # Site and list admins are treated equal so that list admin can pass - # site admin test. :-( - user_or_siteadmin_context = (config.AuthUser,) - is_user_or_siteadmin = mlist.WebAuthenticate( - user_or_siteadmin_context, password, user) - # Authenticate, possibly using the password supplied in the login page - if not is_user_or_siteadmin and \ - not mlist.WebAuthenticate((config.AuthListAdmin, - config.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: - mlog.error('Login failure with private rosters: %s', user) - user = None - loginpage(mlist, doc, user, language) - 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(config.AuthUser, user) - loginpage(mlist, doc, user, language) - 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'): - # Only the user or site administrator can view all subscriptions. - if not is_user_or_siteadmin: - doc.addError(_("""The list administrator may not view the other - subscriptions for this user."""), _('Note: ')) - options_page(mlist, doc, user, cpuser, userlang) - print doc.Format() - return - hostname = mlist.host_name - title = _('List subscriptions for %(safeuser)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. The - # list admin is /not/ allowed to make global changes. - globally = cgidata.getvalue('changeaddr-globally') - if globally and not is_user_or_siteadmin: - doc.addError(_("""The list administrator may not change the names - or addresses for this user's other subscriptions. However, the - subscription for this mailing list has been changed."""), - _('Note: ')) - globally = False - # 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 == cpuser: - 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 -%(safeuser)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 - if set_address: - if cpuser is None: - cpuser = user - # 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(cpuser, newaddr, globally) - mlist.Save() - finally: - mlist.Unlock() - except Errors.InvalidEmailAddress: - msg = _('Invalid email address provided') - except Errors.MMAlreadyAMember: - msg = _('%(newaddr)s is already a member of the list.') - except Errors.MembershipIsBanned: - owneraddr = mlist.GetOwnerEmail() - msg = _("""%(newaddr)s is banned from this list. If you - think this restriction is erroneous, please contact - the list owners at %(owneraddr)s.""") - - 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, however - # the list admin is /not/ allowed to change passwords globally. - pw_globally = cgidata.getvalue('pw-globally') - if pw_globally and not is_user_or_siteadmin: - doc.addError(_("""The list administrator may not change the - password for this user's other subscriptions. However, the - password for this mailing list has been changed."""), - _('Note: ')) - pw_globally = False - - mlists = [mlist] - - if pw_globally: - mlists.extend(lists_of_member(mlist, user)) - - pw = passwords.make_secret(newpw, config.PASSWORD_SCHEME) - for gmlist in mlists: - change_password(gmlist, user, pw) - - # Regenerate the cookie so a re-authorization isn't necessary - print mlist.MakeCookie(config.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 - - mlist.Lock() - needapproval = False - try: - try: - mlist.DeleteMember( - user, 'via the member options page', userack=1) - except Errors.MMNeedApproval: - needapproval = True - 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') - - 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', config.Digests), - ('mime', config.DisableMime), - ('dontreceive', config.DontReceiveOwnPosts), - ('ackposts', config.AcknowledgePosts), - ('disablemail', config.DisableDelivery), - ('conceal', config.ConcealSubscription), - ('remind', config.SuppressPasswordReminder), - ('rcvtopic', config.ReceiveNonmatchingTopics), - ('nodupes', config.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 == config.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 == config.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.language_codes: - 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, list): - # 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: - # `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 == config.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 == config.DisableDelivery: - globalopts.enable = newval - break - - if cgidata.getvalue('remind-globally'): - for flag, newval in newvals: - if flag == config.SuppressPasswordReminder: - globalopts.remind = newval - break - - if cgidata.getvalue('nodupes-globally'): - for flag, newval in newvals: - if flag == config.DontReceiveDuplicates: - globalopts.nodupes = newval - break - - if cgidata.getvalue('mime-globally'): - for flag, newval in newvals: - if flag == config.DisableMime: - globalopts.mime = newval - break - - # Change options globally, but only if this is the user or site admin, - # /not/ if this is the list admin. - if globalopts: - if not is_user_or_siteadmin: - doc.addError(_("""The list administrator may not change the - options for this user's other subscriptions. However the - options for this mailing list subscription has been - changed."""), _('Note: ')) - else: - 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 - - if mlist.isMember(user): - options_page(mlist, doc, user, cpuser, userlang) - else: - loginpage(mlist, doc, user, 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( - config.Digests, 1, user) - replacements['<mm-undigest-radio-button>'] = mlist.FormatOptionButton( - config.Digests, 0, user) - replacements['<mm-plain-digests-button>'] = mlist.FormatOptionButton( - config.DisableMime, 1, user) - replacements['<mm-mime-digests-button>'] = mlist.FormatOptionButton( - config.DisableMime, 0, user) - replacements['<mm-global-mime-button>'] = ( - CheckBox('mime-globally', 1, checked=0).Format()) - replacements['<mm-delivery-enable-button>'] = mlist.FormatOptionButton( - config.DisableDelivery, 0, user) - replacements['<mm-delivery-disable-button>'] = mlist.FormatOptionButton( - config.DisableDelivery, 1, user) - replacements['<mm-disabled-notice>'] = mlist.FormatDisabledNotice(user) - replacements['<mm-dont-ack-posts-button>'] = mlist.FormatOptionButton( - config.AcknowledgePosts, 0, user) - replacements['<mm-ack-posts-button>'] = mlist.FormatOptionButton( - config.AcknowledgePosts, 1, user) - replacements['<mm-receive-own-mail-button>'] = mlist.FormatOptionButton( - config.DontReceiveOwnPosts, 0, user) - replacements['<mm-dont-receive-own-mail-button>'] = ( - mlist.FormatOptionButton(config.DontReceiveOwnPosts, 1, user)) - replacements['<mm-dont-get-password-reminder-button>'] = ( - mlist.FormatOptionButton(config.SuppressPasswordReminder, 1, user)) - replacements['<mm-get-password-reminder-button>'] = ( - mlist.FormatOptionButton(config.SuppressPasswordReminder, 0, user)) - replacements['<mm-public-subscription-button>'] = ( - mlist.FormatOptionButton(config.ConcealSubscription, 0, user)) - replacements['<mm-hide-subscription-button>'] = mlist.FormatOptionButton( - config.ConcealSubscription, 1, user) - replacements['<mm-dont-receive-duplicates-button>'] = ( - mlist.FormatOptionButton(config.DontReceiveDuplicates, 1, user)) - replacements['<mm-receive-duplicates-button>'] = ( - mlist.FormatOptionButton(config.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(config.PENDING_REQUEST_LIFE / config.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: - if emptyflag: - continue - 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(config.ReceiveNonmatchingTopics, 0, user)) - replacements['<mm-receive-nonmatching-topics>'] = ( - mlist.FormatOptionButton(config.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, lang): - 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: - safeuser = Utils.websafe(user) - title = _('%(realname)s list: member options for user %(safeuser)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 - 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=config.WEB_HEADER_COLOR) - if len(mlist.language_codes) > 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=config.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=config.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 config.list_manager.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): - # Must own the list lock! - mlist.Lock() - try: - # 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: - if globalopts.enable is not None: - mlist.setDeliveryStatus(user, globalopts.enable) - - if globalopts.remind is not None: - mlist.setMemberOption(user, config.SuppressPasswordReminder, - globalopts.remind) - - if globalopts.nodupes is not None: - mlist.setMemberOption(user, config.DontReceiveDuplicates, - globalopts.nodupes) - - if globalopts.mime is not None: - mlist.setMemberOption(user, config.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=config.WEB_SUBHEADER_COLOR) - table.AddRow([Bold(Label(_('Name:'))), - Utils.websafe(name)]) - table.AddRow([Bold(Label(_('Pattern (as regexp):'))), - '<pre>' + Utils.websafe(OR.join(pattern.splitlines())) - + '</pre>']) - table.AddRow([Bold(Label(_('Description:'))), - Utils.websafe(description)]) - # Make colors look nice - for row in range(1, 4): - table.AddCellInfo(row, 0, bgcolor=config.WEB_ADMINITEM_COLOR) - - options_page(mlist, doc, user, cpuser, userlang, table.Format()) - print doc.Format() diff --git a/mailman/web/Cgi/private.py b/mailman/web/Cgi/private.py deleted file mode 100644 index 87cad7966..000000000 --- a/mailman/web/Cgi/private.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Provide a password-interface wrapper around private archives.""" - -import os -import sys -import cgi -import logging -import mimetypes - -from Mailman import Errors -from Mailman import MailList -from Mailman import Utils -from Mailman import i18n -from Mailman.configuration import config -from Mailman.htmlformat import * - -# Set up i18n. Until we know which list is being requested, we use the -# server's default. -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -SLASH = '/' - -log = logging.getLogger('mailman.error') -mlog = logging.getLogger('mailman.mischief') - - - -def true_path(path): - "Ensure that the path is safe by removing .." - # Workaround for path traverse vulnerability. Unsuccessful attempts will - # be logged in logs/error. - parts = [x for x in path.split(SLASH) if x not in ('.', '..')] - return SLASH.join(parts)[1:] - - - -def guess_type(url, strict): - if hasattr(mimetypes, 'common_types'): - return mimetypes.guess_type(url, strict) - return mimetypes.guess_type(url) - - - -def main(): - doc = Document() - doc.set_language(config.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') - tpath = true_path(path) - if tpath <> path[1:]: - msg = _('Private archive - "./" and "../" not allowed in URL.') - doc.SetTitle(msg) - doc.AddItem(Header(2, msg)) - print doc.Format() - mlog.error('Private archive hostile path: %s', path) - return - # BAW: This needs to be converted to the Site module abstraction - true_filename = os.path.join( - config.PRIVATE_ARCHIVE_FILE_DIR, tpath) - - 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() - log.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((config.AuthUser, - config.AuthListModerator, - config.AuthListAdmin, - config.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' - # Put the original full path in the authorization form, but avoid - # trailing slash if we're not adding parts. We add it below. - action = mlist.GetScriptURL('private') - if parts[1:]: - action = os.path.join(action, SLASH.join(parts[1:])) - # If we added '/index.html' to true_filename, add a slash to the URL. - # We need this because we no longer add the trailing slash in the - # private.html template. It's always OK to test parts[-1] since we've - # already verified parts[0] is listname. The basic rule is if the - # post URL (action) is a directory, it must be slash terminated, but - # not if it's a file. Otherwise, relative links in the target archive - # page don't work. - if true_filename.endswith('/index.html') and parts[-1] <> 'index.html': - action += SLASH - # Escape web input parameter to avoid cross-site scripting. - print Utils.maketext( - 'private.html', - {'action' : Utils.websafe(action), - '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, enc = guess_type(path, strict=0) - if ctype is None: - ctype = 'text/html' - if mboxfile: - f = open(os.path.join(mlist.archive_dir() + '.mbox', - mlist.internal_name() + '.mbox')) - ctype = 'text/plain' - elif true_filename.endswith('.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() - log.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/web/Cgi/rmlist.py b/mailman/web/Cgi/rmlist.py deleted file mode 100644 index 4b62f09fb..000000000 --- a/mailman/web/Cgi/rmlist.py +++ /dev/null @@ -1,243 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Remove/delete mailing lists through the web.""" - -import os -import cgi -import sys -import errno -import shutil -import logging - -from Mailman import Errors -from Mailman import MailList -from Mailman import Utils -from Mailman import i18n -from Mailman.configuration import config -from Mailman.htmlformat import * - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -log = logging.getLogger('mailman.error') -mlog = logging.getLogger('mailman.mischief') - - - -def main(): - doc = Document() - doc.set_language(config.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() - log.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() - log.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 config.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() - mlog.error('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((config.AuthCreator, - config.AuthListAdmin, - config.AuthSiteAdmin), - password) == config.UnAuthorized: - request_deletion( - doc, mlist, - _('You are not authorized to delete this mailing list')) - return - - # Do the MTA-specific list deletion tasks - if config.MTA: - modname = 'Mailman.MTA.' + config.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(config.VAR_DIR, 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 - log.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 - log.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=config.WEB_HEADER_COLOR) - if not problems: - table.AddRow([_('''You have successfully deleted the mailing list - <b>%(listname)s</b>.''')]) - else: - sitelist = mlist.no_reply_address - 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=config.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 = config.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/web/Cgi/roster.py b/mailman/web/Cgi/roster.py deleted file mode 100644 index 2351d6915..000000000 --- a/mailman/web/Cgi/roster.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""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 os -import cgi -import sys -import urllib -import logging - -from Mailman import Errors -from Mailman import MailList -from Mailman import Utils -from Mailman import i18n -from Mailman.configuration import config -from Mailman.htmlformat import * - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -log = logging.getLogger('mailman.error') - - - -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>')) - log.error('roster: no such list "%s": %s', listname, e) - return - - cgidata = cgi.FieldStorage() - - # messages in form should go in selected language (if any...) - lang = cgidata.getvalue('language') - if lang not in config.languages.enabled_codes: - 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((config.AuthUser, - config.AuthListModerator, - config.AuthListAdmin, - config.AuthSiteAdmin), - password, addr) - else: - # Admin only, so we can ignore the address field - password = cgidata.getvalue('roster-pw', '') - ok = mlist.WebAuthenticate((config.AuthListModerator, - config.AuthListAdmin, - config.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(config.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/web/Cgi/subscribe.py b/mailman/web/Cgi/subscribe.py deleted file mode 100644 index 0eefd0111..000000000 --- a/mailman/web/Cgi/subscribe.py +++ /dev/null @@ -1,252 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Process subscription or roster requests from listinfo form.""" - -from __future__ import with_statement - -import os -import cgi -import sys -import logging - -from Mailman import Errors -from Mailman import MailList -from Mailman import Message -from Mailman import Utils -from Mailman import i18n -from Mailman.UserDesc import UserDesc -from Mailman.configuration import config -from Mailman.htmlformat import * - -SLASH = '/' -ERRORSEP = '\n\n<p>' - -# Set up i18n -_ = i18n._ -i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - -log = logging.getLogger('mailman.error') -mlog = logging.getLogger('mailman.mischief') - - - -def main(): - doc = Document() - doc.set_language(config.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() - log.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') - if language not in config.languages.enabled_codes: - language = mlist.preferred_language - i18n.set_language(language) - doc.set_language(language) - - mlist.Lock() - try: - 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(): - mlog.error('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.InvalidEmailAddress: - results = _('The email address you supplied is not valid.') - 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) - with i18n.using_language(mlang): - 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) - 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() diff --git a/mailman/web/Cgi/wsgi_app.py b/mailman/web/Cgi/wsgi_app.py deleted file mode 100644 index b22dbf452..000000000 --- a/mailman/web/Cgi/wsgi_app.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright (C) 2006-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -import os -import re -import sys - -from cStringIO import StringIO -from email import message_from_string -from urlparse import urlparse - -from Mailman.configuration import config - -# XXX Should this be configurable in Defaults.py? -STEALTH_MODE = False -MOVED_RESPONSE = '302 Found' -# Above is for debugging convenience. We should use: -# MOVED_RESPONSE = '301 Moved Permanently' - - - -def websafe(s): - return s - - -SCRIPTS = ['admin', 'admindb', 'confirm', 'create', - 'edithtml', 'listinfo', 'options', 'private', - 'rmlist', 'roster', 'subscribe'] -ARCHVIEW = ['private'] - -SLASH = '/' -NL2 = '\n\n' -CRLF2 = '\r\n\r\n' - -dotonly = re.compile(r'^\.+$') - - - -# WSGI to CGI wrapper. Mostly copied from scripts/driver. -def mailman_app(environ, start_response): - """Wrapper to *.py CGI commands""" - global STEALTH_MODE, websafe - try: - try: - if not STEALTH_MODE: - from Mailman.Utils import websafe - except: - STEALTH_MODE = True - raise - - import logging - log = logging.getLogger('mailman.error') - - from Mailman import i18n - i18n.set_language(config.DEFAULT_SERVER_LANGUAGE) - - path = environ['PATH_INFO'] - paths = path.split(SLASH) - # sanity check for paths - spaths = [ i for i in paths[1:] if i and not dotonly.match(i) ] - if spaths and spaths != paths[1:]: - newpath = SLASH + SLASH.join(spaths) - start_response(MOVED_RESPONSE, [('Location', newpath)]) - return 'Location: ' + newpath - # find script name - for script in SCRIPTS: - if script in spaths: - # Get script position in spaths and break. - scrpos = spaths.index(script) - break - else: - # Can't find valid script. - start_response('404 Not Found', []) - return '404 Not Found' - # Compose CGI SCRIPT_NAME and PATH_INFO from WSGI path. - script_name = SLASH + SLASH.join(spaths[:scrpos+1]) - environ['SCRIPT_NAME'] = script_name - if len(paths) > scrpos+2: - path_info = SLASH + SLASH.join(paths[scrpos+2:]) - if script in ARCHVIEW \ - and path_info.count('/') in (1,2) \ - and not paths[-1].split('.')[-1] in ('html', 'txt', 'gz'): - # Add index.html if /private/listname or - # /private/listname/YYYYmm is requested. - newpath = script_name + path_info + '/index.html' - start_response(MOVED_RESPONSE, [('Location', newpath)]) - return 'Location: ' + newpath - environ['PATH_INFO'] = path_info - else: - environ['PATH_INFO'] = '' - # Reverse proxy environment. - if environ.has_key('HTTP_X_FORWARDED_HOST'): - environ['HTTP_HOST'] = environ['HTTP_X_FORWARDED_HOST'] - if environ.has_key('HTTP_X_FORWARDED_FOR'): - environ['REMOTE_HOST'] = environ['HTTP_X_FORWARDED_FOR'] - modname = 'Mailman.Cgi.' + script - # Clear previous cookie before setting new one. - os.environ['HTTP_COOKIE'] = '' - for k, v in environ.items(): - os.environ[k] = str(v) - # Prepare for redirection - save_stdin = sys.stdin - # CGI writes its output to sys.stdout, while wsgi app should - # return (list of) strings. - save_stdout = sys.stdout - save_stderr = sys.stderr - tmpstdout = StringIO() - tmpstderr = StringIO() - response = '' - try: - try: - sys.stdin = environ['wsgi.input'] - sys.stdout = tmpstdout - sys.stderr = tmpstderr - __import__(modname) - sys.modules[modname].main() - response = sys.stdout.getvalue() - finally: - sys.stdin = save_stdin - sys.stdout = save_stdout - sys.stderr = save_stderr - except SystemExit: - sys.stdout.write(tmpstdout.getvalue()) - if response: - try: - head, content = response.split(NL2, 1) - except ValueError: - head, content = response.split(CRLF2, 1) - m = message_from_string(head + CRLF2) - start_response('200 OK', m.items()) - return [content] - else: - # TBD: Error Code/Message - start_response('500 Server Error', []) - return '500 Internal Server Error' - except: - start_response('200 OK', [('Content-Type', 'text/html')]) - retstring = print_traceback(log) - retstring += print_environment(log) - return retstring - - - -# These functions are extracted and modified from scripts/driver. -# -# If possible, we print the error to two places. One will always be stdout -# and the other will be the log file if a log file was created. It is assumed -# that stdout is an HTML sink. -def print_traceback(log=None): - try: - import traceback - except ImportError: - traceback = None - try: - from mailman.version import VERSION - except ImportError: - VERSION = '<undetermined>' - - # Write to the log file first. - if log: - outfp = StringIO() - - print >> outfp, '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@' - print >> outfp, '[----- Mailman Version: %s -----]' % VERSION - print >> outfp, '[----- Traceback ------]' - if traceback: - traceback.print_exc(file=outfp) - else: - print >> outfp, '[failed to import module traceback]' - print >> outfp, '[exc: %s, var: %s]' % sys.exc_info()[0:2] - # Don't use .exception() since that'll give us the exception twice. - # IWBNI we could print directly to the log's stream, or treat a log - # like an output stream. - log.error('%s', outfp.getvalue()) - - # return HTML sink. - htfp = StringIO() - print >> htfp, """\ -<head><title>Bug in Mailman version %(VERSION)s</title></head> -<body bgcolor=#ffffff><h2>Bug in Mailman version %(VERSION)s</h2> -<p><h3>We're sorry, we hit a bug!</h3> -""" % locals() - if not STEALTH_MODE: - print >> htfp, '''<p>If you would like to help us identify the problem, -please email a copy of this page to the webmaster for this site with -a description of what happened. Thanks! - -<h4>Traceback:</h4><p><pre>''' - exc_info = sys.exc_info() - if traceback: - for line in traceback.format_exception(*exc_info): - print >> htfp, websafe(line) - else: - print >> htfp, '[failed to import module traceback]' - print >> htfp, '[exc: %s, var: %s]' %\ - [websafe(x) for x in exc_info[0:2]] - print >> htfp, '\n\n</pre></body>' - else: - print >> htfp, '''<p>Please inform the webmaster for this site of this -problem. Printing of traceback and other system information has been -explicitly inhibited, but the webmaster can find this information in the -Mailman error logs.''' - return htfp.getvalue() - - - -def print_environment(log=None): - try: - import os - except ImportError: - os = None - - if log: - outfp = StringIO() - - # Write some information about our Python executable to the log file. - print >> outfp, '[----- Python Information -----]' - print >> outfp, 'sys.version =', sys.version - print >> outfp, 'sys.executable =', sys.executable - print >> outfp, 'sys.prefix =', sys.prefix - print >> outfp, 'sys.exec_prefix =', sys.exec_prefix - print >> outfp, 'sys.path =', sys.exec_prefix - print >> outfp, 'sys.platform =', sys.platform - - # Write the same information to the HTML sink. - htfp = StringIO() - if not STEALTH_MODE: - print >> htfp, """\ -<p><hr><h4>Python information:</h4> - -<p><table> -<tr><th>Variable</th><th>Value</th></tr> -<tr><td><tt>sys.version</tt></td><td> %s </td></tr> -<tr><td><tt>sys.executable</tt></td><td> %s </td></tr> -<tr><td><tt>sys.prefix</tt></td><td> %s </td></tr> -<tr><td><tt>sys.exec_prefix</tt></td><td> %s </td></tr> -<tr><td><tt>sys.path</tt></td><td> %s </td></tr> -<tr><td><tt>sys.platform</tt></td><td> %s </td></tr> -</table>""" % (sys.version, sys.executable, sys.prefix, - sys.exec_prefix, sys.path, sys.platform) - - # Write environment variables to the log file. - if log: - print >> outfp, '[----- Environment Variables -----]' - if os: - for k, v in os.environ.items(): - print >> outfp, '\t%s: %s' % (k, v) - else: - print >> outfp, '[failed to import module os]' - - # Write environment variables to the HTML sink. - if not STEALTH_MODE: - print >> htfp, """\ -<p><hr><h4>Environment variables:</h4> - -<p><table> -<tr><th>Variable</th><th>Value</th></tr> -""" - if os: - for k, v in os.environ.items(): - print >> htfp, '<tr><td><tt>' + websafe(k) + \ - '</tt></td><td>' + websafe(v) + \ - '</td></tr>' - print >> htfp, '</table>' - else: - print >> htfp, '<p><hr>[failed to import module os]' - - # Dump the log output - if log: - log.error('%s', outfp.getvalue()) - - return htfp.getvalue() diff --git a/mailman/web/Gui/Archive.py b/mailman/web/Gui/Archive.py deleted file mode 100644 index 4090e74e9..000000000 --- a/mailman/web/Gui/Archive.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - - - -class Archive(GUIBase): - def GetConfigCategory(self): - return 'archive', _('Archiving Options') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'archive': - return None - return [ - _("List traffic archival policies."), - - ('archive', config.Toggle, (_('No'), _('Yes')), 0, - _('Archive messages?')), - - ('archive_private', config.Radio, (_('public'), _('private')), 0, - _('Is archive file source for public or private archival?')), - - ('archive_volume_frequency', config.Radio, - (_('Yearly'), _('Monthly'), _('Quarterly'), - _('Weekly'), _('Daily')), - 0, - _('How often should a new archive volume be started?')), - ] diff --git a/mailman/web/Gui/Autoresponse.py b/mailman/web/Gui/Autoresponse.py deleted file mode 100644 index 72ef42cad..000000000 --- a/mailman/web/Gui/Autoresponse.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Administrative GUI for the autoresponder.""" - -from Mailman import Utils -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - -# These are the allowable string substitution variables -ALLOWEDS = ('listname', 'listurl', 'requestemail', 'adminemail', 'owneremail') - - - -class Autoresponse(GUIBase): - def GetConfigCategory(self): - return 'autoreply', _('Auto-responder') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'autoreply': - return None - WIDTH = config.TEXTFIELDWIDTH - - return [ - _("""\ -Auto-responder characteristics.<p> - -In the text fields below, string interpolation is performed with -the following key/value substitutions: -<p><ul> - <li><b>listname</b> - <em>gets the name of the mailing list</em> - <li><b>listurl</b> - <em>gets the list's listinfo URL</em> - <li><b>requestemail</b> - <em>gets the list's -request address</em> - <li><b>owneremail</b> - <em>gets the list's -owner address</em> -</ul> - -<p>For each text field, you can either enter the text directly into the text -box, or you can specify a file on your local system to upload as the text."""), - - ('autorespond_postings', config.Toggle, (_('No'), _('Yes')), 0, - _('''Should Mailman send an auto-response to mailing list - posters?''')), - - ('autoresponse_postings_text', config.FileUpload, - (6, WIDTH), 0, - _('Auto-response text to send to mailing list posters.')), - - ('autorespond_admin', config.Toggle, (_('No'), _('Yes')), 0, - _('''Should Mailman send an auto-response to emails sent to the - -owner address?''')), - - ('autoresponse_admin_text', config.FileUpload, - (6, WIDTH), 0, - _('Auto-response text to send to -owner emails.')), - - ('autorespond_requests', config.Radio, - (_('No'), _('Yes, w/discard'), _('Yes, w/forward')), 0, - _('''Should Mailman send an auto-response to emails sent to the - -request address? If you choose yes, decide whether you want - Mailman to discard the original email, or forward it on to the - system as a normal mail command.''')), - - ('autoresponse_request_text', config.FileUpload, - (6, WIDTH), 0, - _('Auto-response text to send to -request emails.')), - - ('autoresponse_graceperiod', config.Number, 3, 0, - _('''Number of days between auto-responses to either the mailing - list or -request/-owner address from the same poster. Set to - zero (or negative) for no grace period (i.e. auto-respond to - every message).''')), - ] - - def _setValue(self, mlist, property, val, doc): - # Handle these specially because we may need to convert to/from - # external $-string representation. - if property in ('autoresponse_postings_text', - 'autoresponse_admin_text', - 'autoresponse_request_text'): - val = self._convertString(mlist, property, ALLOWEDS, val, doc) - if val is None: - # There was a problem, so don't set it - return - GUIBase._setValue(self, mlist, property, val, doc) diff --git a/mailman/web/Gui/Bounce.py b/mailman/web/Gui/Bounce.py deleted file mode 100644 index a2f6f887a..000000000 --- a/mailman/web/Gui/Bounce.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - - - -class Bounce(GUIBase): - def GetConfigCategory(self): - return 'bounce', _('Bounce processing') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'bounce': - return None - return [ - _("""These policies control the automatic bounce processing system - in Mailman. Here's an overview of how it works. - - <p>When a bounce is received, Mailman tries to extract two pieces - of information from the message: the address of the member the - message was intended for, and the severity of the problem causing - the bounce. The severity can be either <em>hard</em> or - <em>soft</em> meaning either a fatal error occurred, or a - transient error occurred. When in doubt, a hard severity is used. - - <p>If no member address can be extracted from the bounce, then the - bounce is usually discarded. Otherwise, each member is assigned a - <em>bounce score</em> and every time we encounter a bounce from - this member we increment the score. Hard bounces increment by 1 - while soft bounces increment by 0.5. We only increment the bounce - score once per day, so even if we receive ten hard bounces from a - member per day, their score will increase by only 1 for that day. - - <p>When a member's bounce score is greater than the - <a href="?VARHELP=bounce/bounce_score_threshold">bounce score - threshold</a>, the subscription is disabled. Once disabled, the - member will not receive any postings from the list until their - membership is explicitly re-enabled (either by the list - administrator or the user). However, they will receive occasional - reminders that their membership has been disabled, and these - reminders will include information about how to re-enable their - membership. - - <p>You can control both the - <a href="?VARHELP=bounce/bounce_you_are_disabled_warnings">number - of reminders</a> the member will receive and the - <a href="?VARHELP=bounce/bounce_you_are_disabled_warnings_interval" - >frequency</a> with which these reminders are sent. - - <p>There is one other important configuration variable; after a - certain period of time -- during which no bounces from the member - are received -- the bounce information is - <a href="?VARHELP=bounce/bounce_info_stale_after">considered - stale</a> and discarded. Thus by adjusting this value, and the - score threshold, you can control how quickly bouncing members are - disabled. You should tune both of these to the frequency and - traffic volume of your list."""), - - _('Bounce detection sensitivity'), - - ('bounce_processing', config.Toggle, (_('No'), _('Yes')), 0, - _('Should Mailman perform automatic bounce processing?'), - _("""By setting this value to <em>No</em>, you disable all - automatic bounce processing for this list, however bounce - messages will still be discarded so that the list administrator - isn't inundated with them.""")), - - ('bounce_score_threshold', config.Number, 5, 0, - _("""The maximum member bounce score before the member's - subscription is disabled. This value can be a floating point - number."""), - _("""Each subscriber is assigned a bounce score, as a floating - point number. Whenever Mailman receives a bounce from a list - member, that member's score is incremented. Hard bounces (fatal - errors) increase the score by 1, while soft bounces (temporary - errors) increase the score by 0.5. Only one bounce per day - counts against a member's score, so even if 10 bounces are - received for a member on the same day, their score will increase - by just 1. - - This variable describes the upper limit for a member's bounce - score, above which they are automatically disabled, but not - removed from the mailing list.""")), - - ('bounce_info_stale_after', config.Number, 5, 0, - _("""The number of days after which a member's bounce information - is discarded, if no new bounces have been received in the - interim. This value must be an integer.""")), - - ('bounce_you_are_disabled_warnings', config.Number, 5, 0, - _("""How many <em>Your Membership Is Disabled</em> warnings a - disabled member should get before their address is removed from - the mailing list. Set to 0 to immediately remove an address from - the list once their bounce score exceeds the threshold. This - value must be an integer.""")), - - ('bounce_you_are_disabled_warnings_interval', config.Number, 5, 0, - _("""The number of days between sending the <em>Your Membership - Is Disabled</em> warnings. This value must be an integer.""")), - - _('Notifications'), - - ('bounce_unrecognized_goes_to_list_owner', config.Toggle, - (_('No'), _('Yes')), 0, - _('''Should Mailman send you, the list owner, any bounce messages - that failed to be detected by the bounce processor? <em>Yes</em> - is recommended.'''), - _("""While Mailman's bounce detector is fairly robust, it's - impossible to detect every bounce format in the world. You - should keep this variable set to <em>Yes</em> for two reasons: 1) - If this really is a permanent bounce from one of your members, - you should probably manually remove them from your list, and 2) - you might want to send the message on to the Mailman developers - so that this new format can be added to its known set. - - <p>If you really can't be bothered, then set this variable to - <em>No</em> and all non-detected bounces will be discarded - without further processing. - - <p><b>Note:</b> This setting will also affect all messages sent - to your list's -admin address. This address is deprecated and - should never be used, but some people may still send mail to this - address. If this happens, and this variable is set to - <em>No</em> those messages too will get discarded. You may want - to set up an - <a href="?VARHELP=autoreply/autoresponse_admin_text">autoresponse - message</a> for email to the -owner and -admin address.""")), - - ('bounce_notify_owner_on_disable', config.Toggle, - (_('No'), _('Yes')), 0, - _("""Should Mailman notify you, the list owner, when bounces - cause a member's subscription to be disabled?"""), - _("""By setting this value to <em>No</em>, you turn off - notification messages that are normally sent to the list owners - when a member's delivery is disabled due to excessive bounces. - An attempt to notify the member will always be made.""")), - - ('bounce_notify_owner_on_removal', config.Toggle, - (_('No'), _('Yes')), 0, - _("""Should Mailman notify you, the list owner, when bounces - cause a member to be unsubscribed?"""), - _("""By setting this value to <em>No</em>, you turn off - notification messages that are normally sent to the list owners - when a member is unsubscribed due to excessive bounces. An - attempt to notify the member will always be made.""")), - - ] - - def _setValue(self, mlist, property, val, doc): - # Do value conversion from web representation to internal - # representation. - try: - if property == 'bounce_processing': - val = int(val) - elif property == 'bounce_score_threshold': - val = float(val) - elif property == 'bounce_info_stale_after': - val = config.days(int(val)) - elif property == 'bounce_you_are_disabled_warnings': - val = int(val) - elif property == 'bounce_you_are_disabled_warnings_interval': - val = config.days(int(val)) - elif property == 'bounce_notify_owner_on_disable': - val = int(val) - elif property == 'bounce_notify_owner_on_removal': - val = int(val) - except ValueError: - doc.addError( - _("""Bad value for <a href="?VARHELP=bounce/%(property)s" - >%(property)s</a>: %(val)s"""), - tag = _('Error: ')) - return - GUIBase._setValue(self, mlist, property, val, doc) - - def getValue(self, mlist, kind, varname, params): - if varname not in ('bounce_info_stale_after', - 'bounce_you_are_disabled_warnings_interval'): - return None - return int(getattr(mlist, varname) / config.days(1)) diff --git a/mailman/web/Gui/ContentFilter.py b/mailman/web/Gui/ContentFilter.py deleted file mode 100644 index 09817f4aa..000000000 --- a/mailman/web/Gui/ContentFilter.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (C) 2002-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""GUI component managing the content filtering options.""" - -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - -NL = '\n' - - - -class ContentFilter(GUIBase): - def GetConfigCategory(self): - return 'contentfilter', _('Content filtering') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'contentfilter': - return None - WIDTH = config.TEXTFIELDWIDTH - - actions = [_('Discard'), _('Reject'), _('Forward to List Owner')] - if config.OWNERS_CAN_PRESERVE_FILTERED_MESSAGES: - actions.append(_('Preserve')) - - return [ - _("""Policies concerning the content of list traffic. - - <p>Content filtering works like this: when a message is - received by the list and you have enabled content filtering, the - individual attachments are first compared to the - <a href="?VARHELP=contentfilter/filter_mime_types">filter - types</a>. If the attachment type matches an entry in the filter - types, it is discarded. - - <p>Then, if there are <a - href="?VARHELP=contentfilter/pass_mime_types">pass types</a> - defined, any attachment type that does <em>not</em> match a - pass type is also discarded. If there are no pass types defined, - this check is skipped. - - <p>After this initial filtering, any <tt>multipart</tt> - attachments that are empty are removed. If the outer message is - left empty after this filtering, then the whole message is - discarded. - - <p> Then, each <tt>multipart/alternative</tt> section will - be replaced by just the first alternative that is non-empty after - filtering if - <a href="?VARHELP=contentfilter/collapse_alternatives" - >collapse_alternatives</a> is enabled. - - <p>Finally, any <tt>text/html</tt> parts that are left in the - message may be converted to <tt>text/plain</tt> if - <a href="?VARHELP=contentfilter/convert_html_to_plaintext" - >convert_html_to_plaintext</a> is enabled and the site is - configured to allow these conversions."""), - - ('filter_content', config.Radio, (_('No'), _('Yes')), 0, - _("""Should Mailman filter the content of list traffic according - to the settings below?""")), - - ('filter_mime_types', config.Text, (10, WIDTH), 0, - _("""Remove message attachments that have a matching content - type."""), - - _("""Use this option to remove each message attachment that - matches one of these content types. Each line should contain a - string naming a MIME <tt>type/subtype</tt>, - e.g. <tt>image/gif</tt>. Leave off the subtype to remove all - parts with a matching major content type, e.g. <tt>image</tt>. - - <p>Blank lines are ignored. - - <p>See also <a href="?VARHELP=contentfilter/pass_mime_types" - >pass_mime_types</a> for a content type whitelist.""")), - - ('pass_mime_types', config.Text, (10, WIDTH), 0, - _("""Remove message attachments that don't have a matching - content type. Leave this field blank to skip this filter - test."""), - - _("""Use this option to remove each message attachment that does - not have a matching content type. Requirements and formats are - exactly like <a href="?VARHELP=contentfilter/filter_mime_types" - >filter_mime_types</a>. - - <p><b>Note:</b> if you add entries to this list but don't add - <tt>multipart</tt> to this list, any messages with attachments - will be rejected by the pass filter.""")), - - ('filter_filename_extensions', config.Text, (10, WIDTH), 0, - _("""Remove message attachments that have a matching filename - extension."""),), - - ('pass_filename_extensions', config.Text, (10, WIDTH), 0, - _("""Remove message attachments that don't have a matching - filename extension. Leave this field blank to skip this filter - test."""),), - - ('collapse_alternatives', config.Radio, (_('No'), _('Yes')), 0, - _("""Should Mailman collapse multipart/alternative to its - first part content?""")), - - ('convert_html_to_plaintext', config.Radio, (_('No'), _('Yes')), 0, - _("""Should Mailman convert <tt>text/html</tt> parts to plain - text? This conversion happens after MIME attachments have been - stripped.""")), - - ('filter_action', config.Radio, tuple(actions), 0, - - _("""Action to take when a message matches the content filtering - rules."""), - - _("""One of these actions is take when the message matches one of - the content filtering rules, meaning, the top-level - content type matches one of the <a - href="?VARHELP=contentfilter/filter_mime_types" - >filter_mime_types</a>, or the top-level content type does - <strong>not</strong> match one of the - <a href="?VARHELP=contentfilter/pass_mime_types" - >pass_mime_types</a>, or if after filtering the subparts of the - message, the message ends up empty. - - <p>Note this action is not taken if after filtering the message - still contains content. In that case the message is always - forwarded on to the list membership. - - <p>When messages are discarded, a log entry is written - containing the Message-ID of the discarded message. When - messages are rejected or forwarded to the list owner, a reason - for the rejection is included in the bounce message to the - original author. When messages are preserved, they are saved in - a special queue directory on disk for the site administrator to - view (and possibly rescue) but otherwise discarded. This last - option is only available if enabled by the site - administrator.""")), - ] - - def _setValue(self, mlist, property, val, doc): - if property in ('filter_mime_types', 'pass_mime_types'): - types = [] - for spectype in [s.strip() for s in val.splitlines()]: - ok = 1 - slashes = spectype.count('/') - if slashes == 0 and not spectype: - ok = 0 - elif slashes == 1: - maintype, subtype = [s.strip().lower() - for s in spectype.split('/')] - if not maintype or not subtype: - ok = 0 - elif slashes > 1: - ok = 0 - if not ok: - doc.addError(_('Bad MIME type ignored: %(spectype)s')) - else: - types.append(spectype.strip().lower()) - if property == 'filter_mime_types': - mlist.filter_mime_types = types - elif property == 'pass_mime_types': - mlist.pass_mime_types = types - elif property in ('filter_filename_extensions', - 'pass_filename_extensions'): - fexts = [] - for ext in [s.strip() for s in val.splitlines()]: - fexts.append(ext.lower()) - if property == 'filter_filename_extensions': - mlist.filter_filename_extensions = fexts - elif property == 'pass_filename_extensions': - mlist.pass_filename_extensions = fexts - else: - GUIBase._setValue(self, mlist, property, val, doc) - - def getValue(self, mlist, kind, property, params): - if property == 'filter_mime_types': - return NL.join(mlist.filter_mime_types) - if property == 'pass_mime_types': - return NL.join(mlist.pass_mime_types) - if property == 'filter_filename_extensions': - return NL.join(mlist.filter_filename_extensions) - if property == 'pass_filename_extensions': - return NL.join(mlist.pass_filename_extensions) - return None diff --git a/mailman/web/Gui/Digest.py b/mailman/web/Gui/Digest.py deleted file mode 100644 index 821d0684f..000000000 --- a/mailman/web/Gui/Digest.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Administrative GUI for digest deliveries.""" - -from Mailman import Utils -from Mailman.configuration import config -from Mailman.i18n import _ - -# Intra-package import -from Mailman.Gui.GUIBase import GUIBase - -# Common b/w nondigest and digest headers & footers. Personalizations may add -# to this. -ALLOWEDS = ('real_name', 'list_name', 'host_name', 'web_page_url', - 'description', 'info', 'cgiext', '_internal_name', - ) - - - -class Digest(GUIBase): - def GetConfigCategory(self): - return 'digest', _('Digest options') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'digest': - return None - WIDTH = config.TEXTFIELDWIDTH - - info = [ - _("Batched-delivery digest characteristics."), - - ('digestable', config.Toggle, (_('No'), _('Yes')), 1, - _('Can list members choose to receive list traffic ' - 'bunched in digests?')), - - ('digest_is_default', config.Radio, - (_('Regular'), _('Digest')), 0, - _('Which delivery mode is the default for new users?')), - - ('mime_is_default_digest', config.Radio, - (_('Plain'), _('MIME')), 0, - _('When receiving digests, which format is default?')), - - ('digest_size_threshhold', config.Number, 3, 0, - _('How big in Kb should a digest be before it gets sent out?')), - # Should offer a 'set to 0' for no size threshhold. - - ('digest_send_periodic', config.Radio, (_('No'), _('Yes')), 1, - _('Should a digest be dispatched daily when the size threshold ' - "isn't reached?")), - - ('digest_header', config.Text, (4, WIDTH), 0, - _('Header added to every digest'), - _("Text attached (as an initial message, before the table" - " of contents) to the top of digests. ") - + Utils.maketext('headfoot.html', raw=1, mlist=mlist)), - - ('digest_footer', config.Text, (4, WIDTH), 0, - _('Footer added to every digest'), - _("Text attached (as a final message) to the bottom of digests. ") - + Utils.maketext('headfoot.html', raw=1, mlist=mlist)), - - ('digest_volume_frequency', config.Radio, - (_('Yearly'), _('Monthly'), _('Quarterly'), - _('Weekly'), _('Daily')), 0, - _('How often should a new digest volume be started?'), - _('''When a new digest volume is started, the volume number is - incremented and the issue number is reset to 1.''')), - - ('_new_volume', config.Toggle, (_('No'), _('Yes')), 0, - _('Should Mailman start a new digest volume?'), - _('''Setting this option instructs Mailman to start a new volume - with the next digest sent out.''')), - - ('_send_digest_now', config.Toggle, (_('No'), _('Yes')), 0, - _('''Should Mailman send the next digest right now, if it is not - empty?''')), - ] - -## if config.OWNERS_CAN_ENABLE_PERSONALIZATION: -## info.extend([ -## ('digest_personalize', config.Toggle, (_('No'), _('Yes')), 1, - -## _('''Should Mailman personalize each digest delivery? -## This is often useful for announce-only lists, but <a -## href="?VARHELP=digest/digest_personalize">read the details</a> -## section for a discussion of important performance -## issues.'''), - -## _("""Normally, Mailman sends the digest messages to -## the mail server in batches. This is much more efficent -## because it reduces the amount of traffic between Mailman and -## the mail server. - -## <p>However, some lists can benefit from a more personalized -## approach. In this case, Mailman crafts a new message for -## each member on the digest delivery list. Turning this on -## adds a few more expansion variables that can be included in -## the <a href="?VARHELP=digest/digest_header">message header</a> -## and <a href="?VARHELP=digest/digest_footer">message footer</a> -## but it may degrade the performance of your site as -## a whole. - -## <p>You need to carefully consider whether the trade-off is -## worth it, or whether there are other ways to accomplish what -## you want. You should also carefully monitor your system load -## to make sure it is acceptable. - -## <p>These additional substitution variables will be available -## for your headers and footers, when this feature is enabled: - -## <ul><li><b>user_address</b> - The address of the user, -## coerced to lower case. -## <li><b>user_delivered_to</b> - The case-preserved address -## that the user is subscribed with. -## <li><b>user_password</b> - The user's password. -## <li><b>user_name</b> - The user's full name. -## <li><b>user_optionsurl</b> - The url to the user's option -## page. -## """)) -## ]) - - return info - - def _setValue(self, mlist, property, val, doc): - # Watch for the special, immediate action attributes - if property == '_new_volume' and val: - mlist.bump_digest_volume() - volume = mlist.volume - number = mlist.next_digest_number - doc.AddItem(_("""The next digest will be sent as volume - %(volume)s, number %(number)s""")) - elif property == '_send_digest_now' and val: - status = mlist.send_digest_now() - if status: - doc.AddItem(_("""A digest has been sent.""")) - else: - doc.AddItem(_("""There was no digest to send.""")) - else: - # Everything else... - if property in ('digest_header', 'digest_footer'): - val = self._convertString(mlist, property, ALLOWEDS, val, doc) - if val is None: - # There was a problem, so don't set it - return - GUIBase._setValue(self, mlist, property, val, doc) diff --git a/mailman/web/Gui/GUIBase.py b/mailman/web/Gui/GUIBase.py deleted file mode 100644 index b2846eb7b..000000000 --- a/mailman/web/Gui/GUIBase.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright (C) 2002-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Base class for all web GUI components.""" - -import re - -from Mailman import Defaults -from Mailman import Errors -from Mailman import Utils -from Mailman.i18n import _ - -NL = '\n' -BADJOINER = '</code>, <code>' - - - -class GUIBase: - # Providing a common interface for GUI component form processing. Most - # GUI components won't need to override anything, but some may want to - # override _setValue() to provide some specialized processing for some - # attributes. - def _getValidValue(self, mlist, property, wtype, val): - # Coerce and validate the new value. - # - # Radio buttons and boolean toggles both have integral type - if wtype in (Defaults.Radio, Defaults.Toggle): - # Let ValueErrors propagate - return int(val) - # String and Text widgets both just return their values verbatim - # but convert into unicode (for 2.2) - if wtype in (Defaults.String, Defaults.Text): - return unicode(val, Utils.GetCharSet(mlist.preferred_language)) - # This widget contains a single email address - if wtype == Defaults.Email: - # BAW: We must allow blank values otherwise reply_to_address can't - # be cleared. This is currently the only Defaults.Email type - # widget in the interface, so watch out if we ever add any new - # ones. - if val: - # Let InvalidEmailAddress propagate. - Utils.ValidateEmail(val) - return val - # These widget types contain lists of email addresses, one per line. - # The EmailListEx allows each line to contain either an email address - # or a regular expression - if wtype in (Defaults.EmailList, Defaults.EmailListEx): - # BAW: value might already be a list, if this is coming from - # config_list input. Sigh. - if isinstance(val, list): - return val - addrs = [] - for addr in [s.strip() for s in val.split(NL)]: - # Discard empty lines - if not addr: - continue - try: - # This throws an exception if the address is invalid - Utils.ValidateEmail(addr) - except Errors.EmailAddressError: - # See if this is a context that accepts regular - # expressions, and that the re is legal - if wtype == Defaults.EmailListEx and addr.startswith('^'): - try: - re.compile(addr) - except re.error: - raise ValueError - else: - raise - addrs.append(addr) - return addrs - # This is a host name, i.e. verbatim - if wtype == Defaults.Host: - return val - # This is a number, either a float or an integer - if wtype == Defaults.Number: - num = -1 - try: - num = int(val) - except ValueError: - # Let ValueErrors percolate up - num = float(val) - if num < 0: - return getattr(mlist, property) - return num - # This widget is a select box, i.e. verbatim - if wtype == Defaults.Select: - return val - # Checkboxes return a list of the selected items, even if only one is - # selected. - if wtype == Defaults.Checkbox: - if isinstance(val, list): - return val - return [val] - if wtype == Defaults.FileUpload: - return val - if wtype == Defaults.Topics: - return val - if wtype == Defaults.HeaderFilter: - return val - # Should never get here - assert 0, 'Bad gui widget type: %s' % wtype - - def _setValue(self, mlist, property, val, doc): - # Set the value, or override to take special action on the property - if not property.startswith('_') and getattr(mlist, property) <> val: - setattr(mlist, property, val) - - def _postValidate(self, mlist, doc): - # Validate all the attributes for this category - pass - - def _escape(self, property, value): - value = value.replace('<', '<') - return value - - def handleForm(self, mlist, category, subcat, cgidata, doc): - for item in self.GetConfigInfo(mlist, category, subcat): - # Skip descriptions and legacy non-attributes - if not isinstance(item, tuple) or len(item) < 5: - continue - # Unpack the gui item description - property, wtype, args, deps, desc = item[0:5] - # BAW: I know this code is a little crufty but I wanted to - # reproduce the semantics of the original code in admin.py as - # closely as possible, for now. We can clean it up later. - # - # The property may be uploadable... - uploadprop = property + '_upload' - if cgidata.has_key(uploadprop) and cgidata[uploadprop].value: - val = cgidata[uploadprop].value - elif not cgidata.has_key(property): - continue - elif isinstance(cgidata[property], list): - val = [self._escape(property, x.value) - for x in cgidata[property]] - else: - val = self._escape(property, cgidata[property].value) - # Coerce the value to the expected type, raising exceptions if the - # value is invalid. - try: - val = self._getValidValue(mlist, property, wtype, val) - except ValueError: - doc.addError(_('Invalid value for variable: %(property)s')) - # This is the parent of InvalidEmailAddress - except Errors.EmailAddressError: - doc.addError( - _('Bad email address for option %(property)s: %(val)s')) - else: - # Set the attribute, which will normally delegate to the mlist - self._setValue(mlist, property, val, doc) - # Do a final sweep once all the attributes have been set. This is how - # we can do cross-attribute assertions - self._postValidate(mlist, doc) - - # Convenience method for handling $-string attributes - def _convertString(self, mlist, property, alloweds, val, doc): - # Is the list using $-strings? - dollarp = getattr(mlist, 'use_dollar_strings', 0) - if dollarp: - ids = Utils.dollar_identifiers(val) - else: - # %-strings - ids = Utils.percent_identifiers(val) - # Here's the list of allowable interpolations - for allowed in alloweds: - if ids.has_key(allowed): - del ids[allowed] - if ids: - # What's left are not allowed - badkeys = ids.keys() - badkeys.sort() - bad = BADJOINER.join(badkeys) - doc.addError(_( - """The following illegal substitution variables were - found in the <code>%(property)s</code> string: - <code>%(bad)s</code> - <p>Your list may not operate properly until you correct this - problem."""), tag=_('Warning: ')) - return val - # Now if we're still using %-strings, do a roundtrip conversion and - # see if the converted value is the same as the new value. If not, - # then they probably left off a trailing `s'. We'll warn them and use - # the corrected string. - if not dollarp: - fixed = Utils.to_percent(Utils.to_dollar(val)) - if fixed <> val: - doc.addError(_( - """Your <code>%(property)s</code> string appeared to - have some correctable problems in its new value. - The fixed value will be used instead. Please - double check that this is what you intended. - """)) - return fixed - return val diff --git a/mailman/web/Gui/General.py b/mailman/web/Gui/General.py deleted file mode 100644 index 27ef354be..000000000 --- a/mailman/web/Gui/General.py +++ /dev/null @@ -1,464 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""MailList mixin class managing the general options.""" - -import re - -from Mailman import Errors -from Mailman import Utils -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - -OPTIONS = ('hide', 'ack', 'notmetoo', 'nodupes') - - - -class General(GUIBase): - def GetConfigCategory(self): - return 'general', _('General Options') - - def GetConfigInfo(self, mlist, category, subcat): - if category <> 'general': - return None - WIDTH = config.TEXTFIELDWIDTH - - # These are for the default_options checkboxes below. - bitfields = {'hide' : config.ConcealSubscription, - 'ack' : config.AcknowledgePosts, - 'notmetoo' : config.DontReceiveOwnPosts, - 'nodupes' : config.DontReceiveDuplicates - } - bitdescrs = { - 'hide' : _("Conceal the member's address"), - 'ack' : _("Acknowledge the member's posting"), - 'notmetoo' : _("Do not send a copy of a member's own post"), - 'nodupes' : - _('Filter out duplicate messages to list members (if possible)'), - } - - optvals = [mlist.new_member_options & bitfields[o] for o in OPTIONS] - opttext = [bitdescrs[o] for o in OPTIONS] - - rtn = [ - _('''Fundamental list characteristics, including descriptive - info and basic behaviors.'''), - - _('General list personality'), - - ('real_name', config.String, WIDTH, 0, - _('The public name of this list (make case-changes only).'), - _('''The capitalization of this name can be changed to make it - presentable in polite company as a proper noun, or to make an - acronym part all upper case, etc. However, the name will be - advertised as the email address (e.g., in subscribe confirmation - notices), so it should <em>not</em> be otherwise altered. (Email - addresses are not case sensitive, but they are sensitive to - almost everything else :-)''')), - - ('owner', config.EmailList, (3, WIDTH), 0, - _("""The list administrator email addresses. Multiple - administrator addresses, each on separate line is okay."""), - - _('''There are two ownership roles associated with each mailing - list. 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 - <a href="passwords">set a separate moderator password</a>, - and also provide the <a href="?VARHELP=general/moderator">email - addresses of the list moderators</a>. Note that the field you - are changing here specifies the list administrators.''')), - - ('moderator', config.EmailList, (3, WIDTH), 0, - _("""The list moderator email addresses. Multiple - moderator addresses, each on separate line is okay."""), - - _('''There are two ownership roles associated with each mailing - list. 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 - <a href="passwords">set a separate moderator password</a>, - and also provide the email addresses of the list moderators in - this section. Note that the field you are changing here - specifies the list moderators.''')), - - ('description', config.String, WIDTH, 0, - _('A terse phrase identifying this list.'), - - _('''This description is used when the mailing list is listed with - other mailing lists, or in headers, and so forth. It should - be as succinct as you can get it, while still identifying what - the list is.''')), - - ('info', config.Text, (7, WIDTH), 0, - _('''An introductory description - a few paragraphs - about the - list. It will be included, as html, at the top of the listinfo - page. Carriage returns will end a paragraph - see the details - for more info.'''), - _("""The text will be treated as html <em>except</em> that - newlines will be translated to <br> - so you can use links, - preformatted text, etc, but don't put in carriage returns except - where you mean to separate paragraphs. And review your changes - - bad html (like some unterminated HTML constructs) can prevent - display of the entire listinfo page.""")), - - ('subject_prefix', config.String, WIDTH, 0, - _('Prefix for subject line of list postings.'), - _("""This text will be prepended to subject lines of messages - posted to the list, to distinguish mailing list messages in in - mailbox summaries. Brevity is premium here, it's ok to shorten - long mailing list names to something more concise, as long as it - still identifies the mailing list. - You can also add a sequencial number by %%d substitution - directive. eg.; [listname %%d] -> [listname 123] - (listname %%05d) -> (listname 00123) - """)), - - ('anonymous_list', config.Radio, (_('No'), _('Yes')), 0, - _("""Hide the sender of a message, replacing it with the list - address (Removes From, Sender and Reply-To fields)""")), - - _('''<tt>Reply-To:</tt> header munging'''), - - ('first_strip_reply_to', config.Radio, (_('No'), _('Yes')), 0, - _('''Should any existing <tt>Reply-To:</tt> header found in the - original message be stripped? If so, this will be done - regardless of whether an explict <tt>Reply-To:</tt> header is - added by Mailman or not.''')), - - ('reply_goes_to_list', config.Radio, - (_('Poster'), _('This list'), _('Explicit address')), 0, - _('''Where are replies to list messages directed? - <tt>Poster</tt> is <em>strongly</em> recommended for most mailing - lists.'''), - - # Details for reply_goes_to_list - _("""This option controls what Mailman does to the - <tt>Reply-To:</tt> header in messages flowing through this - mailing list. When set to <em>Poster</em>, no <tt>Reply-To:</tt> - header is added by Mailman, although if one is present in the - original message, it is not stripped. Setting this value to - either <em>This list</em> or <em>Explicit address</em> causes - Mailman to insert a specific <tt>Reply-To:</tt> header in all - messages, overriding the header in the original message if - necessary (<em>Explicit address</em> inserts the value of <a - href="?VARHELP=general/reply_to_address">reply_to_address</a>). - - <p>There are many reasons not to introduce or override the - <tt>Reply-To:</tt> header. One is that some posters depend on - their own <tt>Reply-To:</tt> settings to convey their valid - return address. Another is that modifying <tt>Reply-To:</tt> - makes it much more difficult to send private replies. See <a - href="http://www.unicom.com/pw/reply-to-harmful.html">`Reply-To' - Munging Considered Harmful</a> for a general discussion of this - issue. See <a - href="http://www.metasystema.net/essays/reply-to.mhtml">Reply-To - Munging Considered Useful</a> for a dissenting opinion. - - <p>Some mailing lists have restricted posting privileges, with a - parallel list devoted to discussions. Examples are `patches' or - `checkin' lists, where software changes are posted by a revision - control system, but discussion about the changes occurs on a - developers mailing list. To support these types of mailing - lists, select <tt>Explicit address</tt> and set the - <tt>Reply-To:</tt> address below to point to the parallel - list.""")), - - ('reply_to_address', config.Email, WIDTH, 0, - _('Explicit <tt>Reply-To:</tt> header.'), - # Details for reply_to_address - _("""This is the address set in the <tt>Reply-To:</tt> header - when the <a - href="?VARHELP=general/reply_goes_to_list">reply_goes_to_list</a> - option is set to <em>Explicit address</em>. - - <p>There are many reasons not to introduce or override the - <tt>Reply-To:</tt> header. One is that some posters depend on - their own <tt>Reply-To:</tt> settings to convey their valid - return address. Another is that modifying <tt>Reply-To:</tt> - makes it much more difficult to send private replies. See <a - href="http://www.unicom.com/pw/reply-to-harmful.html">`Reply-To' - Munging Considered Harmful</a> for a general discussion of this - issue. See <a - href="http://www.metasystema.net/essays/reply-to.mhtml">Reply-To - Munging Considered Useful</a> for a dissenting opinion. - - <p>Some mailing lists have restricted posting privileges, with a - parallel list devoted to discussions. Examples are `patches' or - `checkin' lists, where software changes are posted by a revision - control system, but discussion about the changes occurs on a - developers mailing list. To support these types of mailing - lists, specify the explicit <tt>Reply-To:</tt> address here. You - must also specify <tt>Explicit address</tt> in the - <tt>reply_goes_to_list</tt> - variable. - - <p>Note that if the original message contains a - <tt>Reply-To:</tt> header, it will not be changed.""")), - - _('Umbrella list settings'), - - ('umbrella_list', config.Radio, (_('No'), _('Yes')), 0, - _('''Send password reminders to, eg, "-owner" address instead of - directly to user.'''), - - _("""Set this to yes when this list is intended to cascade only - to other mailing lists. When set, meta notices like - confirmations and password reminders will be directed to an - address derived from the member\'s address - it will have the - value of "umbrella_member_suffix" appended to the member's - account name.""")), - - ('umbrella_member_suffix', config.String, WIDTH, 0, - _('''Suffix for use when this list is an umbrella for other - lists, according to setting of previous "umbrella_list" - setting.'''), - - _("""When "umbrella_list" is set to indicate that this list has - other mailing lists as members, then administrative notices like - confirmations and password reminders need to not be sent to the - member list addresses, but rather to the owner of those member - lists. In that case, the value of this setting is appended to - the member's account name for such notices. `-owner' is the - typical choice. This setting has no effect when "umbrella_list" - is "No".""")), - - _('Notifications'), - - ('send_reminders', config.Radio, (_('No'), _('Yes')), 0, - _('''Send monthly password reminders?'''), - - _('''Turn this on if you want password reminders to be sent once - per month to your members. Note that members may disable their - own individual password reminders.''')), - - ('welcome_msg', config.Text, (4, WIDTH), 0, - _('''List-specific text prepended to new-subscriber welcome - message'''), - - _("""This value, if any, will be added to the front of the - new-subscriber welcome message. The rest of the welcome message - already describes the important addresses and URLs for the - mailing list, so you don't need to include any of that kind of - stuff here. This should just contain mission-specific kinds of - things, like etiquette policies or team orientation, or that kind - of thing. - - <p>Note that this text will be wrapped, according to the - following rules: - <ul><li>Each paragraph is filled so that no line is longer than - 70 characters. - <li>Any line that begins with whitespace is not filled. - <li>A blank line separates paragraphs. - </ul>""")), - - ('send_welcome_msg', config.Radio, (_('No'), _('Yes')), 0, - _('Send welcome message to newly subscribed members?'), - _("""Turn this off only if you plan on subscribing people manually - and don't want them to know that you did so. This option is most - useful for transparently migrating lists from some other mailing - list manager to Mailman.""")), - - ('goodbye_msg', config.Text, (4, WIDTH), 0, - _('''Text sent to people leaving the list. If empty, no special - text will be added to the unsubscribe message.''')), - - ('send_goodbye_msg', config.Radio, (_('No'), _('Yes')), 0, - _('Send goodbye message to members when they are unsubscribed?')), - - ('admin_immed_notify', config.Radio, (_('No'), _('Yes')), 0, - _('''Should the list moderators get immediate notice of new - requests, as well as daily notices about collected ones?'''), - - _('''List moderators (and list administrators) are sent daily - reminders of requests pending approval, like subscriptions to a - moderated list, or postings that are being held for one reason or - another. Setting this option causes notices to be sent - immediately on the arrival of new requests as well.''')), - - ('admin_notify_mchanges', config.Radio, (_('No'), _('Yes')), 0, - _('''Should administrator get notices of subscribes and - unsubscribes?''')), - - ('respond_to_post_requests', config.Radio, - (_('No'), _('Yes')), 0, - _('Send mail to poster when their posting is held for approval?') - ), - - _('Additional settings'), - - ('emergency', config.Toggle, (_('No'), _('Yes')), 0, - _('Emergency moderation of all list traffic.'), - _("""When this option is enabled, all list traffic is emergency - moderated, i.e. held for moderation. Turn this option on when - your list is experiencing a flamewar and you want a cooling off - period.""")), - - ('new_member_options', config.Checkbox, - (opttext, optvals, 0, OPTIONS), - # The description for new_member_options includes a kludge where - # we add a hidden field so that even when all the checkboxes are - # deselected, the form data will still have a new_member_options - # key (it will always be a list). Otherwise, we'd never be able - # to tell if all were deselected! - 0, _('''Default options for new members joining this list.<input - type="hidden" name="new_member_options" value="ignore">'''), - - _("""When a new member is subscribed to this list, their initial - set of options is taken from the this variable's setting.""")), - - ('administrivia', config.Radio, (_('No'), _('Yes')), 0, - _('''(Administrivia filter) Check postings and intercept ones - that seem to be administrative requests?'''), - - _("""Administrivia tests will check postings to see whether it's - really meant as an administrative request (like subscribe, - unsubscribe, etc), and will add it to the the administrative - requests queue, notifying the administrator of the new request, - in the process.""")), - - ('max_message_size', config.Number, 7, 0, - _('''Maximum length in kilobytes (KB) of a message body. Use 0 - for no limit.''')), - - ('host_name', config.Host, WIDTH, 0, - _('Host name this list prefers for email.'), - - _("""The "host_name" is the preferred name for email to - mailman-related addresses on this host, and generally should be - the mail host's exchanger address, if any. This setting can be - useful for selecting among alternative names of a host that has - multiple addresses.""")), - - ] - - if config.ALLOW_RFC2369_OVERRIDES: - rtn.append( - ('include_rfc2369_headers', config.Radio, - (_('No'), _('Yes')), 0, - _("""Should messages from this mailing list include the - <a href="http://www.faqs.org/rfcs/rfc2369.html">RFC 2369</a> - (i.e. <tt>List-*</tt>) headers? <em>Yes</em> is highly - recommended."""), - - _("""RFC 2369 defines a set of List-* headers that are - normally added to every message sent to the list membership. - These greatly aid end-users who are using standards compliant - mail readers. They should normally always be enabled. - - <p>However, not all mail readers are standards compliant yet, - and if you have a large number of members who are using - non-compliant mail readers, they may be annoyed at these - headers. You should first try to educate your members as to - why these headers exist, and how to hide them in their mail - clients. As a last resort you can disable these headers, but - this is not recommended (and in fact, your ability to disable - these headers may eventually go away).""")) - ) - # Suppression of List-Post: headers - rtn.append( - ('include_list_post_header', config.Radio, - (_('No'), _('Yes')), 0, - _('Should postings include the <tt>List-Post:</tt> header?'), - _("""The <tt>List-Post:</tt> header is one of the headers - recommended by - <a href="http://www.faqs.org/rfcs/rfc2369.html">RFC 2369</a>. - However for some <em>announce-only</em> mailing lists, only a - very select group of people are allowed to post to the list; the - general membership is usually not allowed to post. For lists of - this nature, the <tt>List-Post:</tt> header is misleading. - Select <em>No</em> to disable the inclusion of this header. (This - does not affect the inclusion of the other <tt>List-*:</tt> - headers.)""")) - ) - - # Discard held messages after this number of days - rtn.append( - ('max_days_to_hold', config.Number, 7, 0, - _("""Discard held messages older than this number of days. - Use 0 for no automatic discarding.""")) - ) - - return rtn - - def _setValue(self, mlist, property, val, doc): - if property == 'real_name' and \ - val.lower() <> mlist.internal_name().lower(): - # These values can't differ by other than case - doc.addError(_("""<b>real_name</b> attribute not - changed! It must differ from the list's name by case - only.""")) - elif property == 'new_member_options': - newopts = 0 - for opt in OPTIONS: - bitfield = config.OPTINFO[opt] - if opt in val: - newopts |= bitfield - mlist.new_member_options = newopts - elif property == 'subject_prefix': - # Convert any html entities to Unicode - mlist.subject_prefix = Utils.canonstr( - val, mlist.preferred_language) - else: - GUIBase._setValue(self, mlist, property, val, doc) - - def _escape(self, property, value): - # The 'info' property allows HTML, but lets sanitize it to avoid XSS - # exploits. Everything else should be fully escaped. - if property <> 'info': - return GUIBase._escape(self, property, value) - # Sanitize <script> and </script> tags but nothing else. Not the best - # solution, but expedient. - return re.sub(r'<([/]?script.*?)>', r'<\1>', value) - - def _postValidate(self, mlist, doc): - if not mlist.reply_to_address.strip() and \ - mlist.reply_goes_to_list == 2: - # You can't go to an explicit address that is blank - doc.addError(_("""You cannot add a Reply-To: to an explicit - address if that address is blank. Resetting these values.""")) - mlist.reply_to_address = '' - mlist.reply_goes_to_list = 0 - - def getValue(self, mlist, kind, varname, params): - if varname <> 'subject_prefix': - return None - # The subject_prefix may be Unicode - return Utils.uncanonstr(mlist.subject_prefix, mlist.preferred_language) diff --git a/mailman/web/Gui/Language.py b/mailman/web/Gui/Language.py deleted file mode 100644 index 05824be5e..000000000 --- a/mailman/web/Gui/Language.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""MailList mixin class managing the language options.""" - -import codecs - -from Mailman import Utils -from Mailman import i18n -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config - -_ = i18n._ - - - -class Language(GUIBase): - def GetConfigCategory(self): - return 'language', _('Language options') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'language': - return None - # Set things up for the language choices - langs = mlist.language_codes - langnames = [_(description) for description in config.enabled_names] - try: - langi = langs.index(mlist.preferred_language) - except ValueError: - # Someone must have deleted the list's preferred language. Could - # be other trouble lurking! - langi = 0 - # Only allow the admin to choose a language if the system has a - # charset for it. I think this is the best way to test for that. - def checkcodec(charset): - try: - codecs.lookup(charset) - return 1 - except LookupError: - return 0 - - all = sorted(code for code in config.languages.enabled_codes - if checkcodec(Utils.GetCharSet(code))) - checked = [L in langs for L in all] - allnames = [_(config.languages.get_description(code)) for code in all] - return [ - _('Natural language (internationalization) options.'), - - ('preferred_language', config.Select, - (langs, langnames, langi), - 0, - _('Default language for this list.'), - _('''This is the default natural language for this mailing list. - If <a href="?VARHELP=language/available_languages">more than one - language</a> is supported then users will be able to select their - own preferences for when they interact with the list. All other - interactions will be conducted in the default language. This - applies to both web-based and email-based messages, but not to - email posted by list members.''')), - - ('available_languages', config.Checkbox, - (allnames, checked, 0, all), 0, - _('Languages supported by this list.'), - - _('''These are all the natural languages supported by this list. - Note that the - <a href="?VARHELP=language/preferred_language">default - language</a> must be included.''')), - - ('encode_ascii_prefixes', config.Radio, - (_('Never'), _('Always'), _('As needed')), 0, - _("""Encode the - <a href="?VARHELP=general/subject_prefix">subject - prefix</a> even when it consists of only ASCII characters?"""), - - _("""If your mailing list's default language uses a non-ASCII - character set and the prefix contains non-ASCII characters, the - prefix will always be encoded according to the relevant - standards. However, if your prefix contains only ASCII - characters, you may want to set this option to <em>Never</em> to - disable prefix encoding. This can make the subject headers - slightly more readable for users with mail readers that don't - properly handle non-ASCII encodings. - - <p>Note however, that if your mailing list receives both encoded - and unencoded subject headers, you might want to choose <em>As - needed</em>. Using this setting, Mailman will not encode ASCII - prefixes when the rest of the header contains only ASCII - characters, but if the original header contains non-ASCII - characters, it will encode the prefix. This avoids an ambiguity - in the standards which could cause some mail readers to display - extra, or missing spaces between the prefix and the original - header.""")), - - ] - - def _setValue(self, mlist, prop, val, doc): - # If we're changing the list's preferred language, change the I18N - # context as well - if prop == 'preferred_language': - i18n.set_language(val) - doc.set_language(val) - # Language codes must be wrapped - if prop == 'available_languages': - mlist.set_languages(*val) - else: - GUIBase._setValue(self, mlist, prop, val, doc) - - def getValue(self, mlist, kind, varname, params): - if varname == 'available_languages': - # Unwrap Language instances, to return just the code - return [language.code for language in mlist.available_languages] - # Returning None tells the infrastructure to use getattr - return None diff --git a/mailman/web/Gui/Membership.py b/mailman/web/Gui/Membership.py deleted file mode 100644 index bbfdb438b..000000000 --- a/mailman/web/Gui/Membership.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""MailList mixin class managing the membership pseudo-options.""" - -from Mailman.i18n import _ - - - -class Membership: - def GetConfigCategory(self): - return 'members', _('Membership Management...') - - def GetConfigSubCategories(self, category): - if category == 'members': - return [('list', _('Membership List')), - ('add', _('Mass Subscription')), - ('remove', _('Mass Removal')), - ] - return None diff --git a/mailman/web/Gui/NonDigest.py b/mailman/web/Gui/NonDigest.py deleted file mode 100644 index 92fb768ad..000000000 --- a/mailman/web/Gui/NonDigest.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""GUI component for managing the non-digest delivery options.""" - -from Mailman import Utils -from Mailman import Defaults -from Mailman.i18n import _ -from Mailman.configuration import config -from Mailman.Gui.GUIBase import GUIBase - -from Mailman.Gui.Digest import ALLOWEDS -PERSONALIZED_ALLOWEDS = ('user_address', 'user_delivered_to', 'user_password', - 'user_name', 'user_optionsurl', - ) - - - -class NonDigest(GUIBase): - def GetConfigCategory(self): - return 'nondigest', _('Non-digest options') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'nondigest': - return None - WIDTH = config.TEXTFIELDWIDTH - - info = [ - _("Policies concerning immediately delivered list traffic."), - - ('nondigestable', Defaults.Toggle, (_('No'), _('Yes')), 1, - _("""Can subscribers choose to receive mail immediately, rather - than in batched digests?""")), - ] - - if config.OWNERS_CAN_ENABLE_PERSONALIZATION: - info.extend([ - ('personalize', Defaults.Radio, - (_('No'), _('Yes'), _('Full Personalization')), 1, - - _('''Should Mailman personalize each non-digest delivery? - This is often useful for announce-only lists, but <a - href="?VARHELP=nondigest/personalize">read the details</a> - section for a discussion of important performance - issues.'''), - - _("""Normally, Mailman sends the regular delivery messages to - the mail server in batches. This is much more efficent - because it reduces the amount of traffic between Mailman and - the mail server. - - <p>However, some lists can benefit from a more personalized - approach. In this case, Mailman crafts a new message for - each member on the regular delivery list. Turning this - feature on may degrade the performance of your site, so you - need to carefully consider whether the trade-off is worth it, - or whether there are other ways to accomplish what you want. - You should also carefully monitor your system load to make - sure it is acceptable. - - <p>Select <em>No</em> to disable personalization and send - messages to the members in batches. Select <em>Yes</em> to - personalize deliveries and allow additional substitution - variables in message headers and footers (see below). In - addition, by selecting <em>Full Personalization</em>, the - <code>To</code> header of posted messages will be modified to - include the member's address instead of the list's posting - address. - - <p>When personalization is enabled, a few more expansion - variables that can be included in the <a - href="?VARHELP=nondigest/msg_header">message header</a> and - <a href="?VARHELP=nondigest/msg_footer">message footer</a>. - - <p>These additional substitution variables will be available - for your headers and footers, when this feature is enabled: - - <ul><li><b>user_address</b> - The address of the user, - coerced to lower case. - <li><b>user_delivered_to</b> - The case-preserved address - that the user is subscribed with. - <li><b>user_password</b> - The user's password. - <li><b>user_name</b> - The user's full name. - <li><b>user_optionsurl</b> - The url to the user's option - page. - </ul> - """)) - ]) - # BAW: for very dumb reasons, we want the `personalize' attribute to - # show up before the msg_header and msg_footer attrs, otherwise we'll - # get a bogus warning if the header/footer contains a personalization - # substitution variable, and we're transitioning from no - # personalization to personalization enabled. - headfoot = Utils.maketext('headfoot.html', mlist=mlist, raw=1) - if config.OWNERS_CAN_ENABLE_PERSONALIZATION: - extra = _("""\ -When <a href="?VARHELP=nondigest/personalize">personalization</a> is enabled -for this list, additional substitution variables are allowed in your headers -and footers: - -<ul><li><b>user_address</b> - The address of the user, - coerced to lower case. - <li><b>user_delivered_to</b> - The case-preserved address - that the user is subscribed with. - <li><b>user_password</b> - The user's password. - <li><b>user_name</b> - The user's full name. - <li><b>user_optionsurl</b> - The url to the user's option - page. -</ul> -""") - else: - extra = '' - - info.extend([('msg_header', Defaults.Text, (10, WIDTH), 0, - _('Header added to mail sent to regular list members'), - _('''Text prepended to the top of every immediately-delivery - message. ''') + headfoot + extra), - - ('msg_footer', Defaults.Text, (10, WIDTH), 0, - _('Footer added to mail sent to regular list members'), - _('''Text appended to the bottom of every immediately-delivery - message. ''') + headfoot + extra), - ]) - - info.extend([ - ('scrub_nondigest', Defaults.Toggle, (_('No'), _('Yes')), 0, - _('Scrub attachments of regular delivery message?'), - _('''When you scrub attachments, they are stored in archive - area and links are made in the message so that the member can - access via web browser. If you want the attachments totally - disappear, you can use content filter options.''')), - ]) - return info - - def _setValue(self, mlist, property, val, doc): - alloweds = list(ALLOWEDS) - if mlist.personalize: - alloweds.extend(PERSONALIZED_ALLOWEDS) - if property in ('msg_header', 'msg_footer'): - val = self._convertString(mlist, property, alloweds, val, doc) - if val is None: - # There was a problem, so don't set it - return - GUIBase._setValue(self, mlist, property, val, doc) diff --git a/mailman/web/Gui/Passwords.py b/mailman/web/Gui/Passwords.py deleted file mode 100644 index b2fea0fb5..000000000 --- a/mailman/web/Gui/Passwords.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""MailList mixin class managing the password pseudo-options.""" - -from Mailman.i18n import _ -from Mailman.Gui.GUIBase import GUIBase - - - -class Passwords(GUIBase): - def GetConfigCategory(self): - return 'passwords', _('Passwords') - - def handleForm(self, mlist, category, subcat, cgidata, doc): - # Nothing more needs to be done - pass diff --git a/mailman/web/Gui/Privacy.py b/mailman/web/Gui/Privacy.py deleted file mode 100644 index 8d60f9203..000000000 --- a/mailman/web/Gui/Privacy.py +++ /dev/null @@ -1,537 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""MailList mixin class managing the privacy options.""" - -import re - -from Mailman import Utils -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - - - -class Privacy(GUIBase): - def GetConfigCategory(self): - return 'privacy', _('Privacy options...') - - def GetConfigSubCategories(self, category): - if category == 'privacy': - return [('subscribing', _('Subscription rules')), - ('sender', _('Sender filters')), - ('recipient', _('Recipient filters')), - ('spam', _('Spam filters')), - ] - return None - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'privacy': - return None - # Pre-calculate some stuff. Technically, we shouldn't do the - # sub_cfentry calculation here, but it's too ugly to indent it any - # further, and besides, that'll mess up i18n catalogs. - WIDTH = config.TEXTFIELDWIDTH - if config.ALLOW_OPEN_SUBSCRIBE: - sub_cfentry = ('subscribe_policy', config.Radio, - # choices - (_('None'), - _('Confirm'), - _('Require approval'), - _('Confirm and approve')), - 0, - _('What steps are required for subscription?<br>'), - _('''None - no verification steps (<em>Not - Recommended </em>)<br> - Confirm (*) - email confirmation step required <br> - Require approval - require list administrator - Approval for subscriptions <br> - Confirm and approve - both confirm and approve - - <p>(*) when someone requests a subscription, - Mailman sends them a notice with a unique - subscription request number that they must reply to - in order to subscribe.<br> - - This prevents mischievous (or malicious) people - from creating subscriptions for others without - their consent.''')) - else: - sub_cfentry = ('subscribe_policy', config.Radio, - # choices - (_('Confirm'), - _('Require approval'), - _('Confirm and approve')), - 1, - _('What steps are required for subscription?<br>'), - _('''Confirm (*) - email confirmation required <br> - Require approval - require list administrator - approval for subscriptions <br> - Confirm and approve - both confirm and approve - - <p>(*) when someone requests a subscription, - Mailman sends them a notice with a unique - subscription request number that they must reply to - in order to subscribe.<br> This prevents - mischievous (or malicious) people from creating - subscriptions for others without their consent.''')) - - # some helpful values - admin = mlist.GetScriptURL('admin') - - subscribing_rtn = [ - _("""This section allows you to configure subscription and - membership exposure policy. You can also control whether this - list is public or not. See also the - <a href="%(admin)s/archive">Archival Options</a> section for - separate archive-related privacy settings."""), - - _('Subscribing'), - ('advertised', config.Radio, (_('No'), _('Yes')), 0, - _('''Advertise this list when people ask what lists are on this - machine?''')), - - sub_cfentry, - - ('subscribe_auto_approval', config.EmailListEx, (10, WIDTH), 1, - _("""List of addresses (or regexps) whose subscriptions do not - require approval."""), - - _("""When subscription requires approval, addresses in this list - are allowed to subscribe without administrator approval. Add - addresses one per line. You may begin a line with a ^ character - to designate a (case insensitive) regular expression match.""")), - - ('unsubscribe_policy', config.Radio, (_('No'), _('Yes')), 0, - _("""Is the list moderator's approval required for unsubscription - requests? (<em>No</em> is recommended)"""), - - _("""When members want to leave a list, they will make an - unsubscription request, either via the web or via email. - Normally it is best for you to allow open unsubscriptions so that - users can easily remove themselves from mailing lists (they get - really upset if they can't get off lists!). - - <p>For some lists though, you may want to impose moderator - approval before an unsubscription request is processed. Examples - of such lists include a corporate mailing list that all employees - are required to be members of.""")), - - _('Ban list'), - ('ban_list', config.EmailListEx, (10, WIDTH), 1, - _("""List of addresses which are banned from membership in this - mailing list."""), - - _("""Addresses in this list are banned outright from subscribing - to this mailing list, with no further moderation required. Add - addresses one per line; start the line with a ^ character to - designate a regular expression match.""")), - - _("Membership exposure"), - ('private_roster', config.Radio, - (_('Anyone'), _('List members'), _('List admin only')), 0, - _('Who can view subscription list?'), - - _('''When set, the list of subscribers is protected by member or - admin password authentication.''')), - - ('obscure_addresses', config.Radio, (_('No'), _('Yes')), 0, - _("""Show member addresses so they're not directly recognizable - as email addresses?"""), - _("""Setting this option causes member email addresses to be - transformed when they are presented on list web pages (both in - text and as links), so they're not trivially recognizable as - email addresses. The intention is to prevent the addresses - from being snarfed up by automated web scanners for use by - spammers.""")), - ] - - adminurl = mlist.GetScriptURL('admin') - sender_rtn = [ - _("""When a message is posted to the list, a series of - moderation steps are take to decide whether the a moderator must - first approve the message or not. This section contains the - controls for moderation of both member and non-member postings. - - <p>Member postings are held for moderation if their - <b>moderation flag</b> is turned on. You can control whether - member postings are moderated by default or not. - - <p>Non-member postings can be automatically - <a href="?VARHELP=privacy/sender/accept_these_nonmembers" - >accepted</a>, - <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held for - moderation</a>, - <a href="?VARHELP=privacy/sender/reject_these_nonmembers" - >rejected</a> (bounced), or - <a href="?VARHELP=privacy/sender/discard_these_nonmembers" - >discarded</a>, - either individually or as a group. Any - posting from a non-member who is not explicitly accepted, - rejected, or discarded, will have their posting filtered by the - <a href="?VARHELP=privacy/sender/generic_nonmember_action">general - non-member rules</a>. - - <p>In the text boxes below, add one address per line; start the - line with a ^ character to designate a <a href= - "http://www.python.org/doc/current/lib/module-re.html" - >Python regular expression</a>. When entering backslashes, do so - as if you were using Python raw strings (i.e. you generally just - use a single backslash). - - <p>Note that non-regexp matches are always done first."""), - - _('Member filters'), - - ('default_member_moderation', config.Radio, (_('No'), _('Yes')), - 0, _('By default, should new list member postings be moderated?'), - - _("""Each list member has a <em>moderation flag</em> which says - whether messages from the list member can be posted directly to - the list, or must first be approved by the list moderator. When - the moderation flag is turned on, list member postings must be - approved first. You, the list administrator can decide whether a - specific individual's postings will be moderated or not. - - <p>When a new member is subscribed, their initial moderation flag - takes its value from this option. Turn this option off to accept - member postings by default. Turn this option on to, by default, - moderate member postings first. You can always manually set an - individual member's moderation bit by using the - <a href="%(adminurl)s/members">membership management - screens</a>.""")), - - ('member_moderation_action', config.Radio, - (_('Hold'), _('Reject'), _('Discard')), 0, - _("""Action to take when a moderated member posts to the - list."""), - _("""<ul><li><b>Hold</b> -- this holds the message for approval - by the list moderators. - - <p><li><b>Reject</b> -- this automatically rejects the message by - sending a bounce notice to the post's author. The text of the - bounce notice can be <a - href="?VARHELP=privacy/sender/member_moderation_notice" - >configured by you</a>. - - <p><li><b>Discard</b> -- this simply discards the message, with - no notice sent to the post's author. - </ul>""")), - - ('member_moderation_notice', config.Text, (10, WIDTH), 1, - _("""Text to include in any - <a href="?VARHELP/privacy/sender/member_moderation_action" - >rejection notice</a> to - be sent to moderated members who post to this list.""")), - - _('Non-member filters'), - - ('accept_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings should be - automatically accepted."""), - - _("""Postings from any of these non-members will be automatically - accepted with no further moderation applied. Add member - addresses one per line; start the line with a ^ character to - designate a regular expression match.""")), - - ('hold_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings will be - immediately held for moderation."""), - - _("""Postings from any of these non-members will be immediately - and automatically held for moderation by the list moderators. - The sender will receive a notification message which will allow - them to cancel their held message. Add member addresses one per - line; start the line with a ^ character to designate a regular - expression match.""")), - - ('reject_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings will be - automatically rejected."""), - - _("""Postings from any of these non-members will be automatically - rejected. In other words, their messages will be bounced back to - the sender with a notification of automatic rejection. This - option is not appropriate for known spam senders; their messages - should be - <a href="?VARHELP=privacy/sender/discard_these_nonmembers" - >automatically discarded</a>. - - <p>Add member addresses one per line; start the line with a ^ - character to designate a regular expression match.""")), - - ('discard_these_nonmembers', config.EmailListEx, (10, WIDTH), 1, - _("""List of non-member addresses whose postings will be - automatically discarded."""), - - _("""Postings from any of these non-members will be automatically - discarded. That is, the message will be thrown away with no - further processing or notification. The sender will not receive - a notification or a bounce, however the list moderators can - optionally <a href="?VARHELP=privacy/sender/forward_auto_discards" - >receive copies of auto-discarded messages.</a>. - - <p>Add member addresses one per line; start the line with a ^ - character to designate a regular expression match.""")), - - ('generic_nonmember_action', config.Radio, - (_('Accept'), _('Hold'), _('Reject'), _('Discard')), 0, - _("""Action to take for postings from non-members for which no - explicit action is defined."""), - - _("""When a post from a non-member is received, the message's - sender is matched against the list of explicitly - <a href="?VARHELP=privacy/sender/accept_these_nonmembers" - >accepted</a>, - <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held</a>, - <a href="?VARHELP=privacy/sender/reject_these_nonmembers" - >rejected</a> (bounced), and - <a href="?VARHELP=privacy/sender/discard_these_nonmembers" - >discarded</a> addresses. If no match is found, then this action - is taken.""")), - - ('forward_auto_discards', config.Radio, (_('No'), _('Yes')), 0, - _("""Should messages from non-members, which are automatically - discarded, be forwarded to the list moderator?""")), - - ('nonmember_rejection_notice', config.Text, (10, WIDTH), 1, - _("""Text to include in any rejection notice to be sent to - non-members who post to this list. This notice can include - the list's owner address by %%(listowner)s and replaces the - internally crafted default message.""")), - - ] - - recip_rtn = [ - _("""This section allows you to configure various filters based on - the recipient of the message."""), - - _('Recipient filters'), - - ('require_explicit_destination', config.Radio, - (_('No'), _('Yes')), 0, - _("""Must posts have list named in destination (to, cc) field - (or be among the acceptable alias names, specified below)?"""), - - _("""Many (in fact, most) spams do not explicitly name their - myriad destinations in the explicit destination addresses - in - fact often the To: field has a totally bogus address for - obfuscation. The constraint applies only to the stuff in the - address before the '@' sign, but still catches all such spams. - - <p>The cost is that the list will not accept unhindered any - postings relayed from other addresses, unless - - <ol> - <li>The relaying address has the same name, or - - <li>The relaying address name is included on the options that - specifies acceptable aliases for the list. - - </ol>""")), - - ('acceptable_aliases', config.Text, (4, WIDTH), 0, - _("""Alias names (regexps) which qualify as explicit to or cc - destination names for this list."""), - - _("""Alternate addresses that are acceptable when - `require_explicit_destination' is enabled. This option takes a - list of regular expressions, one per line, which is matched - against every recipient address in the message. The matching is - performed with Python's re.match() function, meaning they are - anchored to the start of the string. - - <p>For backwards compatibility with Mailman 1.1, if the regexp - does not contain an `@', then the pattern is matched against just - the local part of the recipient address. If that match fails, or - if the pattern does contain an `@', then the pattern is matched - against the entire recipient address. - - <p>Matching against the local part is deprecated; in a future - release, the pattern will always be matched against the entire - recipient address.""")), - - ('max_num_recipients', config.Number, 5, 0, - _('Ceiling on acceptable number of recipients for a posting.'), - - _('''If a posting has this number, or more, of recipients, it is - held for admin approval. Use 0 for no ceiling.''')), - ] - - spam_rtn = [ - _("""This section allows you to configure various anti-spam - filters posting filters, which can help reduce the amount of spam - your list members end up receiving. - """), - - _('Header filters'), - - ('header_filter_rules', config.HeaderFilter, 0, 0, - _('Filter rules to match against the headers of a message.'), - - _("""Each header filter rule has two parts, a list of regular - expressions, one per line, and an action to take. Mailman - matches the message's headers against every regular expression in - the rule and if any match, the message is rejected, held, or - discarded based on the action you specify. Use <em>Defer</em> to - temporarily disable a rule. - - You can have more than one filter rule for your list. In that - case, each rule is matched in turn, with processing stopped after - the first match. - - Note that headers are collected from all the attachments - (except for the mailman administrivia message) and - matched against the regular expressions. With this feature, - you can effectively sort out messages with dangerous file - types or file name extensions.""")), - - _('Legacy anti-spam filters'), - - ('bounce_matching_headers', config.Text, (6, WIDTH), 0, - _('Hold posts with header value matching a specified regexp.'), - _("""Use this option to prohibit posts according to specific - header values. The target value is a regular-expression for - matching against the specified header. The match is done - disregarding letter case. Lines beginning with '#' are ignored - as comments. - - <p>For example:<pre>to: .*@public.com </pre> says to hold all - postings with a <em>To:</em> mail header containing '@public.com' - anywhere among the addresses. - - <p>Note that leading whitespace is trimmed from the regexp. This - can be circumvented in a number of ways, e.g. by escaping or - bracketing it.""")), - ] - - if subcat == 'sender': - return sender_rtn - elif subcat == 'recipient': - return recip_rtn - elif subcat == 'spam': - return spam_rtn - else: - return subscribing_rtn - - def _setValue(self, mlist, property, val, doc): - # Ignore any hdrfilter_* form variables - if property.startswith('hdrfilter_'): - return - # For subscribe_policy when ALLOW_OPEN_SUBSCRIBE is true, we need to - # add one to the value because the page didn't present an open list as - # an option. - if property == 'subscribe_policy' and not config.ALLOW_OPEN_SUBSCRIBE: - val += 1 - setattr(mlist, property, val) - - # We need to handle the header_filter_rules widgets specially, but - # everything else can be done by the base class's handleForm() method. - # However, to do this we need an awful hack. _setValue() and - # _getValidValue() will essentially ignore any hdrfilter_* form variables. - # TK: we should call this function only in subcat == 'spam' - def _handleForm(self, mlist, category, subcat, cgidata, doc): - # TK: If there is no hdrfilter_* in cgidata, we should not touch - # the header filter rules. - if not cgidata.has_key('hdrfilter_rebox_01'): - return - # First deal with - rules = [] - # We start i at 1 and keep going until we no longer find items keyed - # with the marked tags. - i = 1 - downi = None - while True: - deltag = 'hdrfilter_delete_%02d' % i - reboxtag = 'hdrfilter_rebox_%02d' % i - actiontag = 'hdrfilter_action_%02d' % i - wheretag = 'hdrfilter_where_%02d' % i - addtag = 'hdrfilter_add_%02d' % i - newtag = 'hdrfilter_new_%02d' % i - uptag = 'hdrfilter_up_%02d' % i - downtag = 'hdrfilter_down_%02d' % i - i += 1 - # Was this a delete? If so, we can just ignore this entry - if cgidata.has_key(deltag): - continue - # Get the data for the current box - pattern = cgidata.getvalue(reboxtag) - try: - action = int(cgidata.getvalue(actiontag)) - # We'll get a TypeError when the actiontag is missing and the - # .getvalue() call returns None. - except (ValueError, TypeError): - action = config.DEFER - if pattern is None: - # We came to the end of the boxes - break - if cgidata.has_key(newtag) and not pattern: - # This new entry is incomplete. - if i == 2: - # OK it is the first. - continue - doc.addError(_("""Header filter rules require a pattern. - Incomplete filter rules will be ignored.""")) - continue - # Make sure the pattern was a legal regular expression - try: - re.compile(pattern) - except (re.error, TypeError): - safepattern = Utils.websafe(pattern) - doc.addError(_("""The header filter rule pattern - '%(safepattern)s' is not a legal regular expression. This - rule will be ignored.""")) - continue - # Was this an add item? - if cgidata.has_key(addtag): - # Where should the new one be added? - where = cgidata.getvalue(wheretag) - if where == 'before': - # Add a new empty rule box before the current one - rules.append(('', config.DEFER, True)) - rules.append((pattern, action, False)) - # Default is to add it after... - else: - rules.append((pattern, action, False)) - rules.append(('', config.DEFER, True)) - # Was this an up movement? - elif cgidata.has_key(uptag): - # As long as this one isn't the first rule, move it up - if rules: - rules.insert(-1, (pattern, action, False)) - else: - rules.append((pattern, action, False)) - # Was this the down movement? - elif cgidata.has_key(downtag): - downi = i - 2 - rules.append((pattern, action, False)) - # Otherwise, just retain this one in the list - else: - rules.append((pattern, action, False)) - # Move any down button filter rule - if downi is not None: - rule = rules[downi] - del rules[downi] - rules.insert(downi+1, rule) - mlist.header_filter_rules = rules - - def handleForm(self, mlist, category, subcat, cgidata, doc): - if subcat == 'spam': - self._handleForm(mlist, category, subcat, cgidata, doc) - # Everything else is dealt with by the base handler - GUIBase.handleForm(self, mlist, category, subcat, cgidata, doc) diff --git a/mailman/web/Gui/Topics.py b/mailman/web/Gui/Topics.py deleted file mode 100644 index 00df988be..000000000 --- a/mailman/web/Gui/Topics.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -import re - -from Mailman import Utils -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - -OR = '|' - - - -class Topics(GUIBase): - def GetConfigCategory(self): - return 'topics', _('Topics') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'topics': - return None - WIDTH = config.TEXTFIELDWIDTH - - return [ - _('List topic keywords'), - - ('topics_enabled', config.Radio, (_('Disabled'), _('Enabled')), 0, - _('''Should the topic filter be enabled or disabled?'''), - - _("""The topic filter categorizes each incoming email message - according to <a - href="http://www.python.org/doc/current/lib/module-re.html">regular - expression filters</a> you specify below. If the message's - <code>Subject:</code> or <code>Keywords:</code> header contains a - match against a topic filter, the message is logically placed - into a topic <em>bucket</em>. Each user can then choose to only - receive messages from the mailing list for a particular topic - bucket (or buckets). Any message not categorized in a topic - bucket registered with the user is not delivered to the list. - - <p>Note that this feature only works with regular delivery, not - digest delivery. - - <p>The body of the message can also be optionally scanned for - <code>Subject:</code> and <code>Keywords:</code> headers, as - specified by the <a - href="?VARHELP=topics/topics_bodylines_limit">topics_bodylines_limit</a> - configuration variable.""")), - - ('topics_bodylines_limit', config.Number, 5, 0, - _('How many body lines should the topic matcher scan?'), - - _("""The topic matcher will scan this many lines of the message - body looking for topic keyword matches. Body scanning stops when - either this many lines have been looked at, or a non-header-like - body line is encountered. By setting this value to zero, no body - lines will be scanned (i.e. only the <code>Keywords:</code> and - <code>Subject:</code> headers will be scanned). By setting this - value to a negative number, then all body lines will be scanned - until a non-header-like line is encountered. - """)), - - ('topics', config.Topics, 0, 0, - _('Topic keywords, one per line, to match against each message.'), - - _("""Each topic keyword is actually a regular expression, which is - matched against certain parts of a mail message, specifically the - <code>Keywords:</code> and <code>Subject:</code> message headers. - Note that the first few lines of the body of the message can also - contain a <code>Keywords:</code> and <code>Subject:</code> - "header" on which matching is also performed.""")), - - ] - - def handleForm(self, mlist, category, subcat, cgidata, doc): - # MAS: Did we come from the authentication page? - if not cgidata.has_key('topic_box_01'): - return - topics = [] - # We start i at 1 and keep going until we no longer find items keyed - # with the marked tags. - i = 1 - while True: - 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 - i += 1 - # Was this a delete? If so, we can just ignore this entry - if cgidata.has_key(deltag): - continue - # Get the data for the current box - name = cgidata.getvalue(boxtag) - pattern = cgidata.getvalue(reboxtag) - desc = cgidata.getvalue(desctag) - if name is None: - # We came to the end of the boxes - break - if cgidata.has_key(newtag) and (not name or not pattern): - # This new entry is incomplete. - doc.addError(_("""Topic specifications require both a name and - a pattern. Incomplete topics will be ignored.""")) - continue - # Make sure the pattern was a legal regular expression - name = Utils.websafe(name) - try: - orpattern = OR.join(pattern.splitlines()) - re.compile(orpattern) - except (re.error, TypeError): - safepattern = Utils.websafe(orpattern) - doc.addError(_("""The topic pattern '%(safepattern)s' is not a - legal regular expression. It will be discarded.""")) - continue - # Was this an add item? - if cgidata.has_key(addtag): - # Where should the new one be added? - where = cgidata.getvalue(wheretag) - if where == 'before': - # Add a new empty topics box before the current one - topics.append(('', '', '', True)) - topics.append((name, pattern, desc, False)) - # Default is to add it after... - else: - topics.append((name, pattern, desc, False)) - topics.append(('', '', '', True)) - # Otherwise, just retain this one in the list - else: - topics.append((name, pattern, desc, False)) - # Add these topics to the mailing list object, and deal with other - # options. - mlist.topics = topics - try: - mlist.topics_enabled = int(cgidata.getvalue( - 'topics_enabled', - mlist.topics_enabled)) - except ValueError: - # BAW: should really print a warning - pass - try: - mlist.topics_bodylines_limit = int(cgidata.getvalue( - 'topics_bodylines_limit', - mlist.topics_bodylines_limit)) - except ValueError: - # BAW: should really print a warning - pass diff --git a/mailman/web/Gui/Usenet.py b/mailman/web/Gui/Usenet.py deleted file mode 100644 index 9c1b50809..000000000 --- a/mailman/web/Gui/Usenet.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -from Mailman.Gui.GUIBase import GUIBase -from Mailman.configuration import config -from Mailman.i18n import _ - - - -class Usenet(GUIBase): - def GetConfigCategory(self): - return 'gateway', _('Mail<->News gateways') - - def GetConfigInfo(self, mlist, category, subcat=None): - if category <> 'gateway': - return None - - WIDTH = config.TEXTFIELDWIDTH - VERTICAL = 1 - - return [ - _('Mail-to-News and News-to-Mail gateway services.'), - - _('News server settings'), - - ('nntp_host', config.String, WIDTH, 0, - _('The hostname of the machine your news server is running on.'), - _('''This value may be either the name of your news server, or - optionally of the format name:port, where port is a port number. - - The news server is not part of Mailman proper. You have to - already have access to an NNTP server, and that NNTP server must - recognize the machine this mailing list runs on as a machine - capable of reading and posting news.''')), - - ('linked_newsgroup', config.String, WIDTH, 0, - _('The name of the Usenet group to gateway to and/or from.')), - - ('gateway_to_news', config.Toggle, (_('No'), _('Yes')), 0, - _('''Should new posts to the mailing list be sent to the - newsgroup?''')), - - ('gateway_to_mail', config.Toggle, (_('No'), _('Yes')), 0, - _('''Should new posts to the newsgroup be sent to the mailing - list?''')), - - _('Forwarding options'), - - ('news_moderation', config.Radio, - (_('None'), _('Open list, moderated group'), _('Moderated')), - VERTICAL, - - _("""The moderation policy of the newsgroup."""), - - _("""This setting determines the moderation policy of the - newsgroup and its interaction with the moderation policy of the - mailing list. This only applies to the newsgroup that you are - gatewaying <em>to</em>, so if you are only gatewaying from - Usenet, or the newsgroup you are gatewaying to is not moderated, - set this option to <em>None</em>. - - <p>If the newsgroup is moderated, you can set this mailing list - up to be the moderation address for the newsgroup. By selecting - <em>Moderated</em>, an additional posting hold will be placed in - the approval process. All messages posted to the mailing list - will have to be approved before being sent on to the newsgroup, - or to the mailing list membership. - - <p><em>Note that if the message has an <tt>Approved</tt> header - with the list's administrative password in it, this hold test - will be bypassed, allowing privileged posters to send messages - directly to the list and the newsgroup.</em> - - <p>Finally, if the newsgroup is moderated, but you want to have - an open posting policy anyway, you should select <em>Open list, - moderated group</em>. The effect of this is to use the normal - Mailman moderation facilities, but to add an <tt>Approved</tt> - header to all messages that are gatewayed to Usenet.""")), - - ('news_prefix_subject_too', config.Toggle, (_('No'), _('Yes')), 0, - _('Prefix <tt>Subject:</tt> headers on postings gated to news?'), - _("""Mailman prefixes <tt>Subject:</tt> headers with - <a href="?VARHELP=general/subject_prefix">text you can - customize</a> and normally, this prefix shows up in messages - gatewayed to Usenet. You can set this option to <em>No</em> to - disable the prefix on gated messages. Of course, if you turn off - normal <tt>Subject:</tt> prefixes, they won't be prefixed for - gated messages either.""")), - - _('Mass catch up'), - - ('_mass_catchup', config.Toggle, (_('No'), _('Yes')), 0, - _('Should Mailman perform a <em>catchup</em> on the newsgroup?'), - _('''When you tell Mailman to perform a catchup on the newsgroup, - this means that you want to start gating messages to the mailing - list with the next new message found. All earlier messages on - the newsgroup will be ignored. This is as if you were reading - the newsgroup yourself, and you marked all current messages as - <em>read</em>. By catching up, your mailing list members will - not see any of the earlier messages.''')), - - ] - - def _setValue(self, mlist, property, val, doc): - # Watch for the special, immediate action attributes - if property == '_mass_catchup' and val: - mlist.usenet_watermark = None - doc.AddItem(_('Mass catchup completed')) - else: - GUIBase._setValue(self, mlist, property, val, doc) - - def _postValidate(self, mlist, doc): - # Make sure that if we're gating, that the newsgroups and host - # information are not blank. - if mlist.gateway_to_news or mlist.gateway_to_mail: - # BAW: It's too expensive and annoying to ensure that both the - # host is valid and that the newsgroup is a valid n.g. on the - # server. This should be good enough. - if not mlist.nntp_host or not mlist.linked_newsgroup: - doc.addError(_("""You cannot enable gatewaying unless both the - <a href="?VARHELP=gateway/nntp_host">news server field</a> and - the <a href="?VARHELP=gateway/linked_newsgroup">linked - newsgroup</a> fields are filled in.""")) - # And reset these values - mlist.gateway_to_news = 0 - mlist.gateway_to_mail = 0 diff --git a/mailman/web/Gui/__init__.py b/mailman/web/Gui/__init__.py deleted file mode 100644 index 2e12526c0..000000000 --- a/mailman/web/Gui/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (C) 2001-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -from Archive import Archive -from Autoresponse import Autoresponse -from Bounce import Bounce -from Digest import Digest -from General import General -from Membership import Membership -from NonDigest import NonDigest -from Passwords import Passwords -from Privacy import Privacy -from Topics import Topics -from Usenet import Usenet -from Language import Language -from ContentFilter import ContentFilter - -# Don't export this symbol outside the package -del GUIBase diff --git a/mailman/web/HTMLFormatter.py b/mailman/web/HTMLFormatter.py deleted file mode 100644 index 594f4adea..000000000 --- a/mailman/web/HTMLFormatter.py +++ /dev/null @@ -1,437 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Routines for presentation of list-specific HTML text.""" - -import re -import time - -from mailman import Defaults -from mailman import MemberAdaptor -from mailman import Utils -from mailman.configuration import config -from mailman.htmlformat import * -from mailman.i18n import _ - - -EMPTYSTRING = '' -BR = '<br>' -NL = '\n' -COMMASPACE = ', ' - - - -class HTMLFormatter: - def GetMailmanFooter(self): - ownertext = COMMASPACE.join([Utils.ObscureEmail(a, 1) - for a in self.owner]) - # Remove the .Format() when htmlformat conversion is done. - realname = self.real_name - hostname = self.host_name - listinfo_link = Link(self.GetScriptURL('listinfo'), - realname).Format() - owner_link = Link('mailto:' + self.GetOwnerEmail(), ownertext).Format() - innertext = _('%(listinfo_link)s list run by %(owner_link)s') - return Container( - '<hr>', - Address( - Container( - innertext, - '<br>', - Link(self.GetScriptURL('admin'), - _('%(realname)s administrative interface')), - _(' (requires authorization)'), - '<br>', - Link(Utils.ScriptURL('listinfo'), - _('Overview of all %(hostname)s mailing lists')), - '<p>', MailmanLogo()))).Format() - - def FormatUsers(self, digest, lang=None): - if lang is None: - lang = self.preferred_language - conceal_sub = Defaults.ConcealSubscription - people = [] - if digest: - digestmembers = self.getDigestMemberKeys() - for dm in digestmembers: - if not self.getMemberOption(dm, conceal_sub): - people.append(dm) - num_concealed = len(digestmembers) - len(people) - else: - members = self.getRegularMemberKeys() - for m in members: - if not self.getMemberOption(m, conceal_sub): - people.append(m) - num_concealed = len(members) - len(people) - if num_concealed == 1: - concealed = _('<em>(1 private member not shown)</em>') - elif num_concealed > 1: - concealed = _( - '<em>(%(num_concealed)d private members not shown)</em>') - else: - concealed = '' - items = [] - people.sort() - obscure = self.obscure_addresses - for person in people: - id = Utils.ObscureEmail(person) - url = self.GetOptionsURL(person, obscure=obscure) - if obscure: - showing = Utils.ObscureEmail(person, for_text=1) - else: - showing = person - got = Link(url, showing) - if self.getDeliveryStatus(person) <> MemberAdaptor.ENABLED: - got = Italic('(', got, ')') - items.append(got) - # Just return the .Format() so this works until I finish - # converting everything to htmlformat... - return concealed + UnorderedList(*tuple(items)).Format() - - def FormatOptionButton(self, option, value, user): - if option == Defaults.DisableDelivery: - optval = self.getDeliveryStatus(user) <> MemberAdaptor.ENABLED - else: - optval = self.getMemberOption(user, option) - if optval == value: - checked = ' CHECKED' - else: - checked = '' - name = { - Defaults.DontReceiveOwnPosts : 'dontreceive', - Defaults.DisableDelivery : 'disablemail', - Defaults.DisableMime : 'mime', - Defaults.AcknowledgePosts : 'ackposts', - Defaults.Digests : 'digest', - Defaults.ConcealSubscription : 'conceal', - Defaults.SuppressPasswordReminder : 'remind', - Defaults.ReceiveNonmatchingTopics : 'rcvtopic', - Defaults.DontReceiveDuplicates : 'nodupes', - }[option] - return '<input type=radio name="%s" value="%d"%s>' % ( - name, value, checked) - - def FormatDigestButton(self): - if self.digest_is_default: - checked = ' CHECKED' - else: - checked = '' - return '<input type=radio name="digest" value="1"%s>' % checked - - def FormatDisabledNotice(self, user): - status = self.getDeliveryStatus(user) - reason = None - info = self.getBounceInfo(user) - if status == MemberAdaptor.BYUSER: - reason = _('; it was disabled by you') - elif status == MemberAdaptor.BYADMIN: - reason = _('; it was disabled by the list administrator') - elif status == MemberAdaptor.BYBOUNCE: - date = time.strftime('%d-%b-%Y', - time.localtime(Utils.midnight(info.date))) - reason = _('''; it was disabled due to excessive bounces. The - last bounce was received on %(date)s''') - elif status == MemberAdaptor.UNKNOWN: - reason = _('; it was disabled for unknown reasons') - if reason: - note = FontSize('+1', _( - 'Note: your list delivery is currently disabled%(reason)s.' - )).Format() - link = Link('#disable', _('Mail delivery')).Format() - mailto = Link('mailto:' + self.GetOwnerEmail(), - _('the list administrator')).Format() - return _('''<p>%(note)s - - <p>You may have disabled list delivery intentionally, - or it may have been triggered by bounces from your email - address. In either case, to re-enable delivery, change the - %(link)s option below. Contact %(mailto)s if you have any - questions or need assistance.''') - elif info and info.score > 0: - # Provide information about their current bounce score. We know - # their membership is currently enabled. - score = info.score - total = self.bounce_score_threshold - return _('''<p>We have received some recent bounces from your - address. Your current <em>bounce score</em> is %(score)s out of a - maximum of %(total)s. Please double check that your subscribed - address is correct and that there are no problems with delivery to - this address. Your bounce score will be automatically reset if - the problems are corrected soon.''') - else: - return '' - - def FormatUmbrellaNotice(self, user, type): - addr = self.GetMemberAdminEmail(user) - if self.umbrella_list: - return _("(Note - you are subscribing to a list of mailing lists, " - "so the %(type)s notice will be sent to the admin address" - " for your membership, %(addr)s.)<p>") - else: - return "" - - def FormatSubscriptionMsg(self): - msg = '' - also = '' - if self.subscribe_policy == 1: - msg += _('''You will be sent email requesting confirmation, to - prevent others from gratuitously subscribing you.''') - elif self.subscribe_policy == 2: - msg += _("""This is a closed list, which means your subscription - will be held for approval. You will be notified of the list - moderator's decision by email.""") - also = _('also ') - elif self.subscribe_policy == 3: - msg += _("""You will be sent email requesting confirmation, to - prevent others from gratuitously subscribing you. Once - confirmation is received, your request will be held for approval - by the list moderator. You will be notified of the moderator's - decision by email.""") - also = _("also ") - if msg: - msg += ' ' - if self.private_roster == 1: - msg += _('''This is %(also)sa private list, which means that the - list of members is not available to non-members.''') - elif self.private_roster: - msg += _('''This is %(also)sa hidden list, which means that the - list of members is available only to the list administrator.''') - else: - msg += _('''This is %(also)sa public list, which means that the - list of members list is available to everyone.''') - if self.obscure_addresses: - msg += _(''' (but we obscure the addresses so they are not - easily recognizable by spammers).''') - - if self.umbrella_list: - sfx = self.umbrella_member_suffix - msg += _("""<p>(Note that this is an umbrella list, intended to - have only other mailing lists as members. Among other things, - this means that your confirmation request will be sent to the - `%(sfx)s' account for your address.)""") - return msg - - def FormatUndigestButton(self): - if self.digest_is_default: - checked = '' - else: - checked = ' CHECKED' - return '<input type=radio name="digest" value="0"%s>' % checked - - def FormatMimeDigestsButton(self): - if self.mime_is_default_digest: - checked = ' CHECKED' - else: - checked = '' - return '<input type=radio name="mime" value="1"%s>' % checked - - def FormatPlainDigestsButton(self): - if self.mime_is_default_digest: - checked = '' - else: - checked = ' CHECKED' - return '<input type=radio name="plain" value="1"%s>' % checked - - def FormatEditingOption(self, lang): - if self.private_roster == 0: - either = _('<b><i>either</i></b> ') - else: - either = '' - realname = self.real_name - - text = (_('''To unsubscribe from %(realname)s, get a password reminder, - or change your subscription options %(either)senter your subscription - email address: - <p><center> ''') - + TextBox('email', size=30).Format() - + ' ' - + SubmitButton('UserOptions', - _('Unsubscribe or edit options')).Format() - + Hidden('language', lang).Format() - + '</center>') - if self.private_roster == 0: - text += _('''<p>... <b><i>or</i></b> select your entry from - the subscribers list (see above).''') - text += _(''' If you leave the field blank, you will be prompted for - your email address''') - return text - - def RestrictedListMessage(self, which, restriction): - if not restriction: - return '' - elif restriction == 1: - return _( - '''(<i>%(which)s is only available to the list - members.</i>)''') - else: - return _('''(<i>%(which)s is only available to the list - administrator.</i>)''') - - def FormatRosterOptionForUser(self, lang): - return self.RosterOption(lang).Format() - - def RosterOption(self, lang): - container = Container() - container.AddItem(Hidden('language', lang)) - if not self.private_roster: - container.AddItem(_("Click here for the list of ") - + self.real_name - + _(" subscribers: ")) - container.AddItem(SubmitButton('SubscriberRoster', - _("Visit Subscriber list"))) - else: - if self.private_roster == 1: - only = _('members') - whom = _('Address:') - else: - only = _('the list administrator') - whom = _('Admin address:') - # Solicit the user and password. - container.AddItem( - self.RestrictedListMessage(_('The subscribers list'), - self.private_roster) - + _(" <p>Enter your ") - + whom[:-1].lower() - + _(" and password to visit" - " the subscribers list: <p><center> ") - + whom - + " ") - container.AddItem(self.FormatBox('roster-email')) - container.AddItem(_("Password: ") - + self.FormatSecureBox('roster-pw') - + " ") - container.AddItem(SubmitButton('SubscriberRoster', - _('Visit Subscriber List'))) - container.AddItem("</center>") - return container - - def FormatFormStart(self, name, extra=''): - base_url = self.GetScriptURL(name) - if extra: - full_url = "%s/%s" % (base_url, extra) - else: - full_url = base_url - return ('<FORM Method=POST ACTION="%s">' % full_url) - - def FormatArchiveAnchor(self): - return '<a href="%s">' % self.GetBaseArchiveURL() - - def FormatFormEnd(self): - return '</FORM>' - - def FormatBox(self, name, size=20, value=''): - return '<INPUT type="Text" name="%s" size="%d" value="%s">' % ( - name, size, value) - - def FormatSecureBox(self, name): - return '<INPUT type="Password" name="%s" size="15">' % name - - def FormatButton(self, name, text='Submit'): - return '<INPUT type="Submit" name="%s" value="%s">' % (name, text) - - def FormatReminder(self, lang): - if self.send_reminders: - return _('Once a month, your password will be emailed to you as' - ' a reminder.') - return '' - - def ParseTags(self, template, replacements, lang=None): - if lang is None: - charset = 'us-ascii' - else: - charset = Utils.GetCharSet(lang) - text = Utils.maketext(template, raw=1, lang=lang, mlist=self) - parts = re.split('(</?[Mm][Mm]-[^>]*>)', text) - i = 1 - while i < len(parts): - tag = parts[i].lower() - if replacements.has_key(tag): - repl = replacements[tag] - if isinstance(repl, str): - repl = unicode(repl, charset, 'replace') - parts[i] = repl - else: - parts[i] = '' - i = i + 2 - return EMPTYSTRING.join(parts) - - # This needs to wait until after the list is inited, so let's build it - # when it's needed only. - def GetStandardReplacements(self, lang=None): - dmember_len = len(self.getDigestMemberKeys()) - member_len = len(self.getRegularMemberKeys()) - # If only one language is enabled for this mailing list, omit the - # language choice buttons. - if len(self.language_codes) == 1: - listlangs = _( - config.languages.get_description(self.preferred_language)) - else: - listlangs = self.GetLangSelectBox(lang).Format() - d = { - '<mm-mailman-footer>' : self.GetMailmanFooter(), - '<mm-list-name>' : self.real_name, - '<mm-email-user>' : self._internal_name, - '<mm-list-description>' : self.description, - '<mm-list-info>' : BR.join(self.info.split(NL)), - '<mm-form-end>' : self.FormatFormEnd(), - '<mm-archive>' : self.FormatArchiveAnchor(), - '</mm-archive>' : '</a>', - '<mm-list-subscription-msg>' : self.FormatSubscriptionMsg(), - '<mm-restricted-list-message>' : \ - self.RestrictedListMessage(_('The current archive'), - self.archive_private), - '<mm-num-reg-users>' : `member_len`, - '<mm-num-digesters>' : `dmember_len`, - '<mm-num-members>' : (`member_len + dmember_len`), - '<mm-posting-addr>' : '%s' % self.GetListEmail(), - '<mm-request-addr>' : '%s' % self.GetRequestEmail(), - '<mm-owner>' : self.GetOwnerEmail(), - '<mm-reminder>' : self.FormatReminder(self.preferred_language), - '<mm-host>' : self.host_name, - '<mm-list-langs>' : listlangs, - } - if config.IMAGE_LOGOS: - d['<mm-favicon>'] = config.IMAGE_LOGOS + config.SHORTCUT_ICON - return d - - def GetAllReplacements(self, lang=None): - """ - returns standard replaces plus formatted user lists in - a dict just like GetStandardReplacements. - """ - if lang is None: - lang = self.preferred_language - d = self.GetStandardReplacements(lang) - d.update({"<mm-regular-users>": self.FormatUsers(0, lang), - "<mm-digest-users>": self.FormatUsers(1, lang)}) - return d - - def GetLangSelectBox(self, lang=None, varname='language'): - if lang is None: - lang = self.preferred_language - # Figure out the available languages - values = self.language_codes - legend = [config.languages.get_description(code) for code in values] - try: - selected = values.index(lang) - except ValueError: - try: - selected = values.index(self.preferred_language) - except ValueError: - selected = config.DEFAULT_SERVER_LANGUAGE - # Return the widget - return SelectOptions(varname, values, legend, selected) diff --git a/mailman/web/__init__.py b/mailman/web/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/mailman/web/__init__.py +++ /dev/null diff --git a/mailman/web/htmlformat.py b/mailman/web/htmlformat.py deleted file mode 100644 index 608d0e647..000000000 --- a/mailman/web/htmlformat.py +++ /dev/null @@ -1,670 +0,0 @@ -# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. -# -# This file is part of GNU Mailman. -# -# GNU Mailman 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 3 of the License, or (at your option) -# any later version. -# -# GNU Mailman 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 -# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. - -"""Library for program-based construction of an HTML documents. - -Encapsulate HTML formatting directives in classes that act as containers -for python and, recursively, for nested HTML formatting objects. -""" - -from Mailman import Defaults -from Mailman import Utils -from Mailman import version -from Mailman.configuration import config -from Mailman.i18n import _ - -SPACE = ' ' -EMPTYSTRING = '' -NL = '\n' - - - -# Format an arbitrary object. -def HTMLFormatObject(item, indent): - "Return a presentation of an object, invoking their Format method if any." - if hasattr(item, 'Format'): - return item.Format(indent) - if isinstance(item, basestring): - return item - return str(item) - -def CaseInsensitiveKeyedDict(d): - result = {} - for (k,v) in d.items(): - result[k.lower()] = v - return result - -# Given references to two dictionaries, copy the second dictionary into the -# first one. -def DictMerge(destination, fresh_dict): - for (key, value) in fresh_dict.items(): - destination[key] = value - -class Table: - def __init__(self, **table_opts): - self.cells = [] - self.cell_info = {} - self.row_info = {} - self.opts = table_opts - - def AddOptions(self, opts): - DictMerge(self.opts, opts) - - # Sets all of the cells. It writes over whatever cells you had there - # previously. - - def SetAllCells(self, cells): - self.cells = cells - - # Add a new blank row at the end - def NewRow(self): - self.cells.append([]) - - # Add a new blank cell at the end - def NewCell(self): - self.cells[-1].append('') - - def AddRow(self, row): - self.cells.append(row) - - def AddCell(self, cell): - self.cells[-1].append(cell) - - def AddCellInfo(self, row, col, **kws): - kws = CaseInsensitiveKeyedDict(kws) - if not self.cell_info.has_key(row): - self.cell_info[row] = { col : kws } - elif self.cell_info[row].has_key(col): - DictMerge(self.cell_info[row], kws) - else: - self.cell_info[row][col] = kws - - def AddRowInfo(self, row, **kws): - kws = CaseInsensitiveKeyedDict(kws) - if not self.row_info.has_key(row): - self.row_info[row] = kws - else: - DictMerge(self.row_info[row], kws) - - # What's the index for the row we just put in? - def GetCurrentRowIndex(self): - return len(self.cells)-1 - - # What's the index for the col we just put in? - def GetCurrentCellIndex(self): - return len(self.cells[-1])-1 - - def ExtractCellInfo(self, info): - valid_mods = ['align', 'valign', 'nowrap', 'rowspan', 'colspan', - 'bgcolor'] - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - if key == 'nowrap': - output = output + ' NOWRAP' - continue - else: - output = output + ' %s="%s"' % (key.upper(), val) - - return output - - def ExtractRowInfo(self, info): - valid_mods = ['align', 'valign', 'bgcolor'] - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - output = output + ' %s="%s"' % (key.upper(), val) - - return output - - def ExtractTableInfo(self, info): - valid_mods = ['align', 'width', 'border', 'cellspacing', 'cellpadding', - 'bgcolor'] - - output = '' - - for (key, val) in info.items(): - if not key in valid_mods: - continue - if key == 'border' and val == None: - output = output + ' BORDER' - continue - else: - output = output + ' %s="%s"' % (key.upper(), val) - - return output - - def FormatCell(self, row, col, indent): - try: - my_info = self.cell_info[row][col] - except: - my_info = None - - output = '\n' + ' '*indent + '<td' - if my_info: - output = output + self.ExtractCellInfo(my_info) - item = self.cells[row][col] - item_format = HTMLFormatObject(item, indent+4) - output = '%s>%s</td>' % (output, item_format) - return output - - def FormatRow(self, row, indent): - try: - my_info = self.row_info[row] - except: - my_info = None - - output = '\n' + ' '*indent + '<tr' - if my_info: - output = output + self.ExtractRowInfo(my_info) - output = output + '>' - - for i in range(len(self.cells[row])): - output = output + self.FormatCell(row, i, indent + 2) - - output = output + '\n' + ' '*indent + '</tr>' - - return output - - def Format(self, indent=0): - output = '\n' + ' '*indent + '<table' - output = output + self.ExtractTableInfo(self.opts) - output = output + '>' - - for i in range(len(self.cells)): - output = output + self.FormatRow(i, indent + 2) - - output = output + '\n' + ' '*indent + '</table>\n' - - return output - - -class Link: - def __init__(self, href, text, target=None): - self.href = href - self.text = text - self.target = target - - def Format(self, indent=0): - texpr = "" - if self.target != None: - texpr = ' target="%s"' % self.target - return '<a href="%s"%s>%s</a>' % (HTMLFormatObject(self.href, indent), - texpr, - HTMLFormatObject(self.text, indent)) - -class FontSize: - """FontSize is being deprecated - use FontAttr(..., size="...") instead.""" - def __init__(self, size, *items): - self.items = list(items) - self.size = size - - def Format(self, indent=0): - output = '<font size="%s">' % self.size - for item in self.items: - output = output + HTMLFormatObject(item, indent) - output = output + '</font>' - return output - -class FontAttr: - """Present arbitrary font attributes.""" - def __init__(self, *items, **kw): - self.items = list(items) - self.attrs = kw - - def Format(self, indent=0): - seq = [] - for k, v in self.attrs.items(): - seq.append('%s="%s"' % (k, v)) - output = '<font %s>' % SPACE.join(seq) - for item in self.items: - output = output + HTMLFormatObject(item, indent) - output = output + '</font>' - return output - - -class Container: - def __init__(self, *items): - if not items: - self.items = [] - else: - self.items = items - - def AddItem(self, obj): - self.items.append(obj) - - def Format(self, indent=0): - output = [] - for item in self.items: - output.append(HTMLFormatObject(item, indent)) - return EMPTYSTRING.join(output) - - -class Label(Container): - align = 'right' - - def __init__(self, *items): - Container.__init__(self, *items) - - def Format(self, indent=0): - return ('<div align="%s">' % self.align) + \ - Container.Format(self, indent) + \ - '</div>' - - -# My own standard document template. YMMV. -# something more abstract would be more work to use... - -class Document(Container): - title = None - language = None - bgcolor = Defaults.WEB_BG_COLOR - suppress_head = 0 - - def set_language(self, lang=None): - self.language = lang - - def set_bgcolor(self, color): - self.bgcolor = color - - def SetTitle(self, title): - self.title = title - - def Format(self, indent=0, **kws): - charset = 'us-ascii' - if self.language: - charset = Utils.GetCharSet(self.language) - output = ['Content-Type: text/html; charset=%s\n' % charset] - if not self.suppress_head: - kws.setdefault('bgcolor', self.bgcolor) - tab = ' ' * indent - output.extend([tab, - '<HTML>', - '<HEAD>' - ]) - if config.IMAGE_LOGOS: - output.append('<LINK REL="SHORTCUT ICON" HREF="%s">' % - (config.IMAGE_LOGOS + config.SHORTCUT_ICON)) - # Hit all the bases - output.append('<META http-equiv="Content-Type" ' - 'content="text/html; charset=%s">' % charset) - if self.title: - output.append('%s<TITLE>%s</TITLE>' % (tab, self.title)) - output.append('%s</HEAD>' % tab) - quals = [] - # Default link colors - if config.WEB_VLINK_COLOR: - kws.setdefault('vlink', config.WEB_VLINK_COLOR) - if config.WEB_ALINK_COLOR: - kws.setdefault('alink', config.WEB_ALINK_COLOR) - if config.WEB_LINK_COLOR: - kws.setdefault('link', config.WEB_LINK_COLOR) - for k, v in kws.items(): - quals.append('%s="%s"' % (k, v)) - output.append('%s<BODY %s>' % (tab, SPACE.join(quals))) - # Always do this... - output.append(Container.Format(self, indent)) - if not self.suppress_head: - output.append('%s</BODY>' % tab) - output.append('%s</HTML>' % tab) - return NL.join(output).encode(charset, 'replace') - - def addError(self, errmsg, tag=None): - if tag is None: - tag = _('Error: ') - self.AddItem(Header(3, Bold(FontAttr( - _(tag), color=config.WEB_ERROR_COLOR, size='+2')).Format() + - Italic(errmsg).Format())) - - -class HeadlessDocument(Document): - """Document without head section, for templates that provide their own.""" - suppress_head = 1 - - -class StdContainer(Container): - def Format(self, indent=0): - # If I don't start a new I ignore indent - output = '<%s>' % self.tag - output = output + Container.Format(self, indent) - output = '%s</%s>' % (output, self.tag) - return output - - -class QuotedContainer(Container): - def Format(self, indent=0): - # If I don't start a new I ignore indent - output = '<%s>%s</%s>' % ( - self.tag, - Utils.websafe(Container.Format(self, indent)), - self.tag) - return output - -class Header(StdContainer): - def __init__(self, num, *items): - self.items = items - self.tag = 'h%d' % num - -class Address(StdContainer): - tag = 'address' - -class Underline(StdContainer): - tag = 'u' - -class Bold(StdContainer): - tag = 'strong' - -class Italic(StdContainer): - tag = 'em' - -class Preformatted(QuotedContainer): - tag = 'pre' - -class Subscript(StdContainer): - tag = 'sub' - -class Superscript(StdContainer): - tag = 'sup' - -class Strikeout(StdContainer): - tag = 'strike' - -class Center(StdContainer): - tag = 'center' - -class Form(Container): - def __init__(self, action='', method='POST', encoding=None, *items): - apply(Container.__init__, (self,) + items) - self.action = action - self.method = method - self.encoding = encoding - - def set_action(self, action): - self.action = action - - def Format(self, indent=0): - spaces = ' ' * indent - encoding = '' - if self.encoding: - encoding = 'enctype="%s"' % self.encoding - output = '\n%s<FORM action="%s" method="%s" %s>\n' % ( - spaces, self.action, self.method, encoding) - output = output + Container.Format(self, indent+2) - output = '%s\n%s</FORM>\n' % (output, spaces) - return output - - -class InputObj: - def __init__(self, name, ty, value, checked, **kws): - self.name = name - self.type = ty - self.value = value - self.checked = checked - self.kws = kws - - def Format(self, indent=0): - output = ['<INPUT name="%s" type="%s" value="%s"' % - (self.name, self.type, self.value)] - for item in self.kws.items(): - output.append('%s="%s"' % item) - if self.checked: - output.append('CHECKED') - output.append('>') - return SPACE.join(output) - - -class SubmitButton(InputObj): - def __init__(self, name, button_text): - InputObj.__init__(self, name, "SUBMIT", button_text, checked=0) - -class PasswordBox(InputObj): - def __init__(self, name, value='', size=Defaults.TEXTFIELDWIDTH): - InputObj.__init__(self, name, "PASSWORD", value, checked=0, size=size) - -class TextBox(InputObj): - def __init__(self, name, value='', size=Defaults.TEXTFIELDWIDTH): - InputObj.__init__(self, name, "TEXT", value, checked=0, size=size) - -class Hidden(InputObj): - def __init__(self, name, value=''): - InputObj.__init__(self, name, 'HIDDEN', value, checked=0) - -class TextArea: - def __init__(self, name, text='', rows=None, cols=None, wrap='soft', - readonly=0): - self.name = name - self.text = text - self.rows = rows - self.cols = cols - self.wrap = wrap - self.readonly = readonly - - def Format(self, indent=0): - output = '<TEXTAREA NAME=%s' % self.name - if self.rows: - output += ' ROWS=%s' % self.rows - if self.cols: - output += ' COLS=%s' % self.cols - if self.wrap: - output += ' WRAP=%s' % self.wrap - if self.readonly: - output += ' READONLY' - output += '>%s</TEXTAREA>' % self.text - return output - -class FileUpload(InputObj): - def __init__(self, name, rows=None, cols=None, **kws): - apply(InputObj.__init__, (self, name, 'FILE', '', 0), kws) - -class RadioButton(InputObj): - def __init__(self, name, value, checked=0, **kws): - apply(InputObj.__init__, (self, name, 'RADIO', value, checked), kws) - -class CheckBox(InputObj): - def __init__(self, name, value, checked=0, **kws): - apply(InputObj.__init__, (self, name, "CHECKBOX", value, checked), kws) - -class VerticalSpacer: - def __init__(self, size=10): - self.size = size - def Format(self, indent=0): - output = '<spacer type="vertical" height="%d">' % self.size - return output - -class WidgetArray: - Widget = None - - def __init__(self, name, button_names, checked, horizontal, values): - self.name = name - self.button_names = button_names - self.checked = checked - self.horizontal = horizontal - self.values = values - assert len(values) == len(button_names) - # Don't assert `checked' because for RadioButtons it is a scalar while - # for CheckedBoxes it is a vector. Subclasses will assert length. - - def ischecked(self, i): - raise NotImplemented - - def Format(self, indent=0): - t = Table(cellspacing=5) - items = [] - for i, name, value in zip(range(len(self.button_names)), - self.button_names, - self.values): - ischecked = (self.ischecked(i)) - item = self.Widget(self.name, value, ischecked).Format() + name - items.append(item) - if not self.horizontal: - t.AddRow(items) - items = [] - if self.horizontal: - t.AddRow(items) - return t.Format(indent) - -class RadioButtonArray(WidgetArray): - Widget = RadioButton - - def __init__(self, name, button_names, checked=None, horizontal=1, - values=None): - if values is None: - values = range(len(button_names)) - # BAW: assert checked is a scalar... - WidgetArray.__init__(self, name, button_names, checked, horizontal, - values) - - def ischecked(self, i): - return self.checked == i - -class CheckBoxArray(WidgetArray): - Widget = CheckBox - - def __init__(self, name, button_names, checked=None, horizontal=0, - values=None): - if checked is None: - checked = [0] * len(button_names) - else: - assert len(checked) == len(button_names) - if values is None: - values = range(len(button_names)) - WidgetArray.__init__(self, name, button_names, checked, horizontal, - values) - - def ischecked(self, i): - return self.checked[i] - -class UnorderedList(Container): - def Format(self, indent=0): - spaces = ' ' * indent - output = '\n%s<ul>\n' % spaces - for item in self.items: - output = output + '%s<li>%s\n' % \ - (spaces, HTMLFormatObject(item, indent + 2)) - output = output + '%s</ul>\n' % spaces - return output - -class OrderedList(Container): - def Format(self, indent=0): - spaces = ' ' * indent - output = '\n%s<ol>\n' % spaces - for item in self.items: - output = output + '%s<li>%s\n' % \ - (spaces, HTMLFormatObject(item, indent + 2)) - output = output + '%s</ol>\n' % spaces - return output - -class DefinitionList(Container): - def Format(self, indent=0): - spaces = ' ' * indent - output = '\n%s<dl>\n' % spaces - for dt, dd in self.items: - output = output + '%s<dt>%s\n<dd>%s\n' % \ - (spaces, HTMLFormatObject(dt, indent+2), - HTMLFormatObject(dd, indent+2)) - output = output + '%s</dl>\n' % spaces - return output - - - -# Logo constants -# -# These are the URLs which the image logos link to. The Mailman home page now -# points at the gnu.org site instead of the www.list.org mirror. - -PYTHON_URL = 'http://www.python.org/' -GNU_URL = 'http://www.gnu.org/' - -# The names of the image logo files. These are concatentated onto -# config.IMAGE_LOGOS (not urljoined). -DELIVERED_BY = 'mailman.jpg' -PYTHON_POWERED = 'PythonPowered.png' -GNU_HEAD = 'gnu-head-tiny.jpg' - - -def MailmanLogo(): - t = Table(border=0, width='100%') - if config.IMAGE_LOGOS: - def logo(file): - return config.IMAGE_LOGOS + file - mmlink = '<img src="%s" alt="Delivered by Mailman" border=0>' \ - '<br>version %s' % (logo(DELIVERED_BY), version.VERSION) - pylink = '<img src="%s" alt="Python Powered" border=0>' % \ - logo(PYTHON_POWERED) - gnulink = '<img src="%s" alt="GNU\'s Not Unix" border=0>' % \ - logo(GNU_HEAD) - t.AddRow([mmlink, pylink, gnulink]) - else: - # use only textual links - version = version.VERSION - mmlink = Link(config.MAILMAN_URL, - _('Delivered by Mailman<br>version %(version)s')) - pylink = Link(PYTHON_URL, _('Python Powered')) - gnulink = Link(GNU_URL, _("Gnu's Not Unix")) - t.AddRow([mmlink, pylink, gnulink]) - return t - - -class SelectOptions: - def __init__(self, varname, values, legend, - selected=0, size=1, multiple=None): - self.varname = varname - self.values = values - self.legend = legend - self.size = size - self.multiple = multiple - # we convert any type to tuple, commas are needed - if not multiple: - if isinstance(selected, int): - self.selected = (selected,) - elif isinstance(selected, tuple): - self.selected = (selected[0],) - elif isinstance(selected, list): - self.selected = (selected[0],) - else: - self.selected = (0,) - - def Format(self, indent=0): - spaces = " " * indent - items = min( len(self.values), len(self.legend) ) - - # jcrey: If there is no argument, we return nothing to avoid errors - if items == 0: - return "" - - text = "\n" + spaces + "<Select name=\"%s\"" % self.varname - if self.size > 1: - text = text + " size=%d" % self.size - if self.multiple: - text = text + " multiple" - text = text + ">\n" - - for i in range(items): - if i in self.selected: - checked = " Selected" - else: - checked = "" - - opt = " <option value=\"%s\"%s> %s </option>" % ( - self.values[i], checked, self.legend[i]) - text = text + spaces + opt + "\n" - - return text + spaces + '</Select>' |
