diff options
Diffstat (limited to 'Mailman/Cgi')
-rw-r--r-- | Mailman/Cgi/admin.py | 150 | ||||
-rw-r--r-- | Mailman/Cgi/admindb.py | 189 | ||||
-rw-r--r-- | Mailman/Cgi/confirm.py | 12 | ||||
-rw-r--r-- | Mailman/Cgi/edithtml.py | 4 | ||||
-rw-r--r-- | Mailman/Cgi/listinfo.py | 29 | ||||
-rw-r--r-- | Mailman/Cgi/options.py | 92 | ||||
-rwxr-xr-x[-rw-r--r--] | Mailman/Cgi/private.py | 25 | ||||
-rw-r--r-- | Mailman/Cgi/rmlist.py | 8 | ||||
-rw-r--r-- | Mailman/Cgi/roster.py | 4 | ||||
-rwxr-xr-x[-rw-r--r--] | Mailman/Cgi/subscribe.py | 46 |
10 files changed, 444 insertions, 115 deletions
diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py index f3284e17..370a2507 100644 --- a/Mailman/Cgi/admin.py +++ b/Mailman/Cgi/admin.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -32,6 +32,7 @@ from email.Utils import unquote, parseaddr, formataddr from Mailman import mm_cfg from Mailman import Utils +from Mailman import Message from Mailman import MailList from Mailman import Errors from Mailman import MemberAdaptor @@ -77,8 +78,8 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' admin_overview(_('No such list <em>%(safelistname)s</em>')) - syslog('error', 'admin.py access for non-existent list: %s', - listname) + syslog('error', 'admin: No such list "%s": %s\n', + listname, e) return # Now that we know what list has been requested, all subsequent admin # pages are shown in that list's preferred language. @@ -88,7 +89,8 @@ def main(): # CSRF check safe_params = ['VARHELP', 'adminpw', 'admlogin', - 'letter', 'chunk', 'findmember'] + 'letter', 'chunk', 'findmember', + 'legend'] params = cgidata.keys() if set(params) - set(safe_params): csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token')) @@ -522,7 +524,7 @@ def show_results(mlist, doc, category, subcat, cgidata): if category == 'members': # Figure out which subcategory we should display subcat = Utils.GetPathPieces()[-1] - if subcat not in ('list', 'add', 'remove'): + if subcat not in ('list', 'add', 'remove', 'change'): subcat = 'list' # Add member category specific tables form.AddItem(membership_options(mlist, subcat, cgidata, doc, form)) @@ -876,6 +878,13 @@ def membership_options(mlist, subcat, cgidata, doc, form): container.AddItem(header) mass_remove(mlist, container) return container + if subcat == 'change': + header.AddRow([Center(Header(2, _('Address Change')))]) + header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2, + bgcolor=mm_cfg.WEB_HEADER_COLOR) + container.AddItem(header) + address_change(mlist, container) + return container # Otherwise... header.AddRow([Center(Header(2, _('Membership List')))]) header.AddCellInfo(header.GetCurrentRowIndex(), 0, colspan=2, @@ -902,6 +911,15 @@ def membership_options(mlist, subcat, cgidata, doc, form): all.sort(lambda x, y: cmp(x.lower(), y.lower())) # See if the query has a regular expression regexp = cgidata.getvalue('findmember', '').strip() + try: + regexp = regexp.decode(Utils.GetCharSet(mlist.preferred_language)) + except UnicodeDecodeError: + # This is probably a non-ascii character and an English language + # (ascii) list. Even if we didn't throw the UnicodeDecodeError, + # the input may have contained mnemonic or numeric HTML entites mixed + # with other characters. Trying to grok the real meaning out of that + # is complex and error prone, so we don't try. + pass if regexp: try: cre = re.compile(regexp, re.IGNORECASE) @@ -1151,7 +1169,8 @@ def membership_options(mlist, subcat, cgidata, doc, form): 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')) + link = Link(url + 'chunk=%d' % i + findfrag, + _('from %(start)s to %(end)s')) buttons.append(link) buttons = UnorderedList(*buttons) container.AddItem(footer + buttons.Format() + '<p>') @@ -1167,7 +1186,8 @@ def mass_subscribe(mlist, container): Label(_('Subscribe these users now or invite them?')), RadioButtonArray('subscribe_or_invite', (_('Subscribe'), _('Invite')), - 0, values=(0, 1)) + mm_cfg.DEFAULT_SUBSCRIBE_OR_INVITE, + values=(0, 1)) ]) table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) @@ -1241,6 +1261,38 @@ def mass_remove(mlist, container): +def address_change(mlist, container): + # ADDRESS CHANGE + GREY = mm_cfg.WEB_ADMINITEM_COLOR + table = Table(width='90%') + table.AddRow([Italic(_("""To change a list member's address, enter the + member's current and new addresses below. Use the check boxes to send + notice of the change to the old and/or new address(es)."""))]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, colspan=3) + table.AddRow([ + Label(_("Member's current address")), + TextBox(name='change_from'), + CheckBox('notice_old', 'yes', 0).Format() + + ' ' + + _('Send notice') + ]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 2, bgcolor=GREY) + table.AddRow([ + Label(_('Address to change to')), + TextBox(name='change_to'), + CheckBox('notice_new', 'yes', 0).Format() + + ' ' + + _('Send notice') + ]) + table.AddCellInfo(table.GetCurrentRowIndex(), 0, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 1, bgcolor=GREY) + table.AddCellInfo(table.GetCurrentRowIndex(), 2, bgcolor=GREY) + container.AddItem(Center(table)) + + + def password_inputs(mlist): adminurl = mlist.GetScriptURL('admin', absolute=1) table = Table(cellspacing=3, cellpadding=4) @@ -1436,10 +1488,12 @@ def change_options(mlist, category, subcat, cgidata, doc): 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) + send_unsub_notifications = safeint( + 'send_unsub_notifications_to_list_owner', + mlist.admin_notify_mchanges) + userack = safeint( + 'send_unsub_ack_to_this_batch', + mlist.send_goodbye_msg) unsubscribe_errors = [] unsubscribe_success = [] for addr in names: @@ -1461,13 +1515,77 @@ def change_options(mlist, category, subcat, cgidata, doc): color='#ff0000', size='+2')).Format())) doc.AddItem(UnorderedList(*unsubscribe_errors)) doc.AddItem('<p>') + # Address Changes + if cgidata.has_key('change_from'): + change_from = cgidata.getvalue('change_from', '') + change_to = cgidata.getvalue('change_to', '') + schange_from = Utils.websafe(change_from) + schange_to = Utils.websafe(change_to) + success = False + msg = None + if not (change_from and change_to): + msg = _('You must provide both current and new addresses.') + elif change_from == change_to: + msg = _('Current and new addresses must be different.') + elif mlist.isMember(change_to): + # ApprovedChangeMemberAddress will just delete the old address + # and we don't want that here. + msg = _('%(schange_to)s is already a list member.') + else: + try: + Utils.ValidateEmail(change_to) + except (Errors.MMBadEmailError, Errors.MMHostileAddress): + msg = _('%(schange_to)s is not a valid email address.') + if msg: + doc.AddItem(Header(3, msg)) + doc.AddItem('<p>') + return + try: + mlist.ApprovedChangeMemberAddress(change_from, change_to, False) + except Errors.NotAMemberError: + msg = _('%(schange_from)s is not a member') + except Errors.MMAlreadyAMember: + msg = _('%(schange_to)s is already a member') + except Errors.MembershipIsBanned, pat: + spat = Utils.websafe(str(pat)) + msg = _('%(schange_to)s matches banned pattern %(spat)s') + else: + msg = _('Address %(schange_from)s changed to %(schange_to)s') + success = True + doc.AddItem(Header(3, msg)) + lang = mlist.getMemberLanguage(change_to) + otrans = i18n.get_translation() + i18n.set_language(lang) + list_name = mlist.getListAddress() + text = Utils.wrap(_("""The member address %(change_from)s on the +%(list_name)s list has been changed to %(change_to)s. +""")) + subject = _('%(list_name)s address change notice.') + i18n.set_translation(otrans) + if success and cgidata.getvalue('notice_old', '') == 'yes': + # Send notice to old address. + msg = Message.UserNotification(change_from, + mlist.GetOwnerEmail(), + text=text, + subject=subject, + lang=lang + ) + msg.send(mlist) + doc.AddItem(Header(3, _('Notification sent to %(schange_from)s.'))) + if success and cgidata.getvalue('notice_new', '') == 'yes': + # Send notice to new address. + msg = Message.UserNotification(change_to, + mlist.GetOwnerEmail(), + text=text, + subject=subject, + lang=lang + ) + msg.send(mlist) + doc.AddItem(Header(3, _('Notification sent to %(schange_to)s.'))) + 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 + val = safeint('allmodbit_val') if val not in (0, 1): doc.addError(_('Bad moderation flag value')) else: diff --git a/Mailman/Cgi/admindb.py b/Mailman/Cgi/admindb.py index d1873321..af3f46d1 100644 --- a/Mailman/Cgi/admindb.py +++ b/Mailman/Cgi/admindb.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -50,16 +50,38 @@ i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) EXCERPT_HEIGHT = 10 EXCERPT_WIDTH = 76 +SSENDER = mm_cfg.SSENDER +SSENDERTIME = mm_cfg.SSENDERTIME +STIME = mm_cfg.STIME +if mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS in (SSENDERTIME, STIME): + ssort = mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS +else: + ssort = SSENDER -def helds_by_sender(mlist): +def helds_by_skey(mlist, ssort=SSENDER): heldmsgs = mlist.GetHeldMessageIds() - bysender = {} + byskey = {} for id in heldmsgs: + ptime = mlist.GetRecord(id)[0] sender = mlist.GetRecord(id)[1] - bysender.setdefault(sender, []).append(id) - return bysender + if ssort in (SSENDER, SSENDERTIME): + skey = (0, sender) + else: + skey = (ptime, sender) + byskey.setdefault(skey, []).append((ptime, id)) + # Sort groups by time + for k, v in byskey.items(): + if len(v) > 1: + v.sort() + byskey[k] = v + if ssort == SSENDERTIME: + # Rekey with time + newkey = (v[0][0], k[1]) + del byskey[k] + byskey[newkey] = v + return byskey def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3): @@ -76,6 +98,7 @@ def hacky_radio_buttons(btnname, labels, values, defaults, spacing=3): def main(): + global ssort # Figure out which list is being requested parts = Utils.GetPathPieces() if not parts: @@ -91,7 +114,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' handle_no_list(_('No such list <em>%(safelistname)s</em>')) - syslog('error', 'No such list "%s": %s\n', listname, e) + syslog('error', 'admindb: No such list "%s": %s\n', listname, e) return # Now that we know which list to use, set the system's language to it. @@ -213,9 +236,11 @@ def main(): nomessages = not mlist.GetHeldMessageIds() if not (details or sender or msgid or nomessages): form.AddItem(Center( + '<label>' + CheckBox('discardalldefersp', 0).Format() + ' ' + - _('Discard all messages marked <em>Defer</em>') + _('Discard all messages marked <em>Defer</em>') + + '</label>' )) # Add a link back to the overview, if we're not viewing the overview! adminurl = mlist.GetScriptURL('admin', absolute=1) @@ -253,7 +278,7 @@ def main(): raw=1, mlist=mlist)) num = show_pending_subs(mlist, form) num += show_pending_unsubs(mlist, form) - num += show_helds_overview(mlist, form) + num += show_helds_overview(mlist, form, ssort) addform = num > 0 # Finish up the document, adding buttons to the form if addform: @@ -261,9 +286,11 @@ def main(): form.AddItem('<hr>') if not (details or sender or msgid or nomessages): form.AddItem(Center( + '<label>' + CheckBox('discardalldefersp', 0).Format() + ' ' + - _('Discard all messages marked <em>Defer</em>') + _('Discard all messages marked <em>Defer</em>') + + '</label>' )) form.AddItem(Center(SubmitButton('submit', _('Submit All Data')))) # Put 'Logout' link before the footer @@ -314,10 +341,10 @@ def show_pending_subs(mlist, form): for id in pendingsubs: addr = mlist.GetRecord(id)[1] byaddrs.setdefault(addr, []).append(id) - addrs = byaddrs.keys() + addrs = byaddrs.items() addrs.sort() num = 0 - for addr, ids in byaddrs.items(): + for addr, ids in addrs: # Eliminate duplicates for id in ids[1:]: mlist.HandleRequest(id, mm_cfg.DISCARD) @@ -334,8 +361,10 @@ def show_pending_subs(mlist, form): mm_cfg.DISCARD), checked=0).Format() if addr not in mlist.ban_list: - radio += '<br>' + CheckBox('ban-%d' % id, 1).Format() + \ - ' ' + _('Permanently ban from this list') + radio += ('<br>' + '<label>' + + CheckBox('ban-%d' % id, 1).Format() + + ' ' + _('Permanently ban from this list') + + '</label>') # 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, Utils.websafe(fullname)), @@ -365,10 +394,10 @@ def show_pending_unsubs(mlist, form): for id in pendingunsubs: addr = mlist.GetRecord(id) byaddrs.setdefault(addr, []).append(id) - addrs = byaddrs.keys() + addrs = byaddrs.items() addrs.sort() num = 0 - for addr, ids in byaddrs.items(): + for addr, ids in addrs: # Eliminate duplicates for id in ids[1:]: mlist.HandleRequest(id, mm_cfg.DISCARD) @@ -402,20 +431,29 @@ def show_pending_unsubs(mlist, form): -def show_helds_overview(mlist, form): - # Sort the held messages by sender - bysender = helds_by_sender(mlist) - if not bysender: +def show_helds_overview(mlist, form, ssort=SSENDER): + # Sort the held messages. + byskey = helds_by_skey(mlist, ssort) + if not byskey: return 0 form.AddItem('<hr>') form.AddItem(Center(Header(2, _('Held Messages')))) + # Add the sort sequence choices if wanted + if mm_cfg.DISPLAY_HELD_SUMMARY_SORT_BUTTONS: + form.AddItem(Center(_('Show this list grouped/sorted by'))) + form.AddItem(Center(hacky_radio_buttons( + 'summary_sort', + (_('sender/sender'), _('sender/time'), _('ungrouped/time')), + (SSENDER, SSENDERTIME, STIME), + (ssort == SSENDER, ssort == SSENDERTIME, ssort == STIME)))) # Add the by-sender overview tables admindburl = mlist.GetScriptURL('admindb', absolute=1) table = Table(border=0) form.AddItem(table) - senders = bysender.keys() - senders.sort() - for sender in senders: + skeys = byskey.keys() + skeys.sort() + for skey in skeys: + sender = skey[1] qsender = quote_plus(sender) esender = Utils.websafe(sender) senderurl = admindburl + '?sender=' + qsender @@ -434,15 +472,19 @@ def show_helds_overview(mlist, form): left.AddRow([btns]) left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) left.AddRow([ + '<label>' + CheckBox('senderpreserve-' + qsender, 1).Format() + ' ' + - _('Preserve messages for the site administrator') + _('Preserve messages for the site administrator') + + '</label>' ]) left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) left.AddRow([ + '<label>' + CheckBox('senderforward-' + qsender, 1).Format() + ' ' + - _('Forward messages (individually) to:') + _('Forward messages (individually) to:') + + '</label>' ]) left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) left.AddRow([ @@ -458,9 +500,11 @@ def show_helds_overview(mlist, form): if mlist.isMember(sender): if mlist.getMemberOption(sender, mm_cfg.Moderate): left.AddRow([ + '<label>' + CheckBox('senderclearmodp-' + qsender, 1).Format() + ' ' + - _("Clear this member's <em>moderate</em> flag") + _("Clear this member's <em>moderate</em> flag") + + '</label>' ]) else: left.AddRow( @@ -471,9 +515,11 @@ def show_helds_overview(mlist, form): mlist.reject_these_nonmembers + mlist.discard_these_nonmembers): left.AddRow([ + '<label>' + CheckBox('senderfilterp-' + qsender, 1).Format() + ' ' + - _('Add <b>%(esender)s</b> to one of these sender filters:') + _('Add <b>%(esender)s</b> to one of these sender filters:') + + '</label>' ]) left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) btns = hacky_radio_buttons( @@ -485,10 +531,11 @@ def show_helds_overview(mlist, form): left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) if sender not in mlist.ban_list: left.AddRow([ + '<label>' + CheckBox('senderbanp-' + qsender, 1).Format() + ' ' + _("""Ban <b>%(esender)s</b> from ever subscribing to this - mailing list""")]) + mailing list""") + '</label>']) left.AddCellInfo(left.GetCurrentRowIndex(), 0, colspan=2) right = Table(border=0) right.AddRow([ @@ -499,7 +546,7 @@ def show_helds_overview(mlist, form): right.AddCellInfo(right.GetCurrentRowIndex(), 0, colspan=2) right.AddRow([' ', ' ']) counter = 1 - for id in bysender[sender]: + for ptime, id in byskey[skey]: info = mlist.GetRecord(id) ptime, sender, subject, reason, filename, msgdata = info # BAW: This is really the size of the message pickle, which should @@ -540,13 +587,14 @@ def show_helds_overview(mlist, form): def show_sender_requests(mlist, form, sender): - bysender = helds_by_sender(mlist) - if not bysender: + byskey = helds_by_skey(mlist, SSENDER) + if not byskey: return - sender_ids = bysender.get(sender) + sender_ids = byskey.get((0, sender)) if sender_ids is None: # BAW: should we print an error message? return + sender_ids = [x[1] for x in sender_ids] total = len(sender_ids) count = 1 for id in sender_ids: @@ -623,13 +671,11 @@ def show_post_requests(mlist, id, info, total, count, form): for line in email.Iterators.body_line_iterator(msg, decode=True): lines.append(line) chars += len(line) - if chars > limit > 0: + if chars >= limit > 0: break - # Negative values mean display the entire message, regardless of size - if limit > 0: - body = EMPTYSTRING.join(lines)[:mm_cfg.ADMINDB_PAGE_TEXT_LIMIT] - else: - body = EMPTYSTRING.join(lines) + # We may have gone over the limit on the last line, but keep the full line + # anyway to avoid losing part of a multibyte character. + body = EMPTYSTRING.join(lines) # Get message charset and try encode in list charset # We get it from the first text part. # We need to replace invalid characters here or we can throw an uncaught @@ -644,7 +690,7 @@ def show_post_requests(mlist, id, info, total, count, form): lcset = Utils.GetCharSet(mlist.preferred_language) if mcset <> lcset: try: - body = unicode(body, mcset).encode(lcset, 'replace') + body = unicode(body, mcset, 'replace').encode(lcset, 'replace') except (LookupError, UnicodeError, ValueError): pass hdrtxt = NL.join(['%s: %s' % (k, v) for k, v in msg.items()]) @@ -677,12 +723,16 @@ def show_post_requests(mlist, id, info, total, count, form): t.AddRow([Bold(_('Action:')), buttons]) t.AddCellInfo(row+3, col-1, align='right') t.AddRow([' ', + '<label>' + CheckBox('preserve-%d' % id, 'on', 0).Format() + - ' ' + _('Preserve message for site administrator') + ' ' + _('Preserve message for site administrator') + + '</label>' ]) t.AddRow([' ', + '<label>' + CheckBox('forward-%d' % id, 'on', 0).Format() + ' ' + _('Additionally, forward this message to: ') + + '</label>' + TextBox('forward-addr-%d' % id, size=47, value=mlist.GetOwnerEmail()).Format() ]) @@ -709,7 +759,9 @@ def show_post_requests(mlist, id, info, total, count, form): def process_form(mlist, doc, cgidata): + global ssort senderactions = {} + badaddrs = [] # Sender-centric actions for k in cgidata.keys(): for prefix in ('senderaction-', 'senderpreserve-', 'senderforward-', @@ -729,6 +781,8 @@ def process_form(mlist, doc, cgidata): discardalldefersp = cgidata.getvalue('discardalldefersp', 0) except ValueError: discardalldefersp = 0 + # Get the summary sequence + ssort = int(cgidata.getvalue('summary_sort', SSENDER)) for sender in senderactions.keys(): actions = senderactions[sender] # Handle what to do about all this sender's held messages @@ -743,8 +797,8 @@ def process_form(mlist, doc, cgidata): preserve = actions.get('senderpreserve', 0) forward = actions.get('senderforward', 0) forwardaddr = actions.get('senderforwardto', '') - bysender = helds_by_sender(mlist) - for id in bysender.get(sender, []): + byskey = helds_by_skey(mlist, SSENDER) + for ptime, id in byskey.get((0, sender), []): if id not in senderactions[sender]['message_ids']: # It arrived after the page was displayed. Skip it. continue @@ -762,20 +816,27 @@ def process_form(mlist, doc, cgidata): # Now see if this sender should be added to one of the nonmember # sender filters. if actions.get('senderfilterp', 0): + # Check for an invalid sender address. try: - which = int(actions.get('senderfilter')) - except ValueError: - # Bogus form - which = 'ignore' - if which == mm_cfg.ACCEPT: - mlist.accept_these_nonmembers.append(sender) - elif which == mm_cfg.HOLD: - mlist.hold_these_nonmembers.append(sender) - elif which == mm_cfg.REJECT: - mlist.reject_these_nonmembers.append(sender) - elif which == mm_cfg.DISCARD: - mlist.discard_these_nonmembers.append(sender) - # Otherwise, it's a bogus form, so ignore it + Utils.ValidateEmail(sender) + except Errors.EmailAddressError: + # Don't check for dups. Report it once for each checked box. + badaddrs.append(sender) + else: + try: + which = int(actions.get('senderfilter')) + except ValueError: + # Bogus form + which = 'ignore' + if which == mm_cfg.ACCEPT: + mlist.accept_these_nonmembers.append(sender) + elif which == mm_cfg.HOLD: + mlist.hold_these_nonmembers.append(sender) + elif which == mm_cfg.REJECT: + mlist.reject_these_nonmembers.append(sender) + elif which == mm_cfg.DISCARD: + mlist.discard_these_nonmembers.append(sender) + # Otherwise, it's a bogus form, so ignore it # And now see if we're to clear the member's moderation flag. if actions.get('senderclearmodp', 0): try: @@ -785,8 +846,15 @@ def process_form(mlist, doc, cgidata): pass # And should this address be banned? if actions.get('senderbanp', 0): - if sender not in mlist.ban_list: - mlist.ban_list.append(sender) + # Check for an invalid sender address. + try: + Utils.ValidateEmail(sender) + except Errors.EmailAddressError: + # Don't check for dups. Report it once for each checked box. + badaddrs.append(sender) + else: + if sender not in mlist.ban_list: + mlist.ban_list.append(sender) # Now, do message specific actions banaddrs = [] erroraddrs = [] @@ -836,6 +904,8 @@ def process_form(mlist, doc, cgidata): if cgidata.getvalue(bankey): sender = mlist.GetRecord(request_id)[1] if sender not in mlist.ban_list: + # We don't need to validate the sender. An invalid address + # can't get here. mlist.ban_list.append(sender) # Handle the request id try: @@ -854,7 +924,14 @@ def process_form(mlist, doc, cgidata): doc.AddItem(Header(2, _('Database Updated...'))) if erroraddrs: for addr in erroraddrs: + addr = Utils.websafe(addr) doc.AddItem(`addr` + _(' is already a member') + '<br>') if banaddrs: for addr, patt in banaddrs: + addr = Utils.websafe(addr) doc.AddItem(_('%(addr)s is banned (matched: %(patt)s)') + '<br>') + if badaddrs: + for addr in badaddrs: + addr = Utils.websafe(addr) + doc.AddItem(`addr` + ': ' + _('Bad/Invalid email address') + + '<br>') diff --git a/Mailman/Cgi/confirm.py b/Mailman/Cgi/confirm.py index 607f1784..97297e10 100644 --- a/Mailman/Cgi/confirm.py +++ b/Mailman/Cgi/confirm.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2011 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -64,7 +64,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' print doc.Format() - syslog('error', 'No such list "%s": %s', listname, e) + syslog('error', 'confirm: No such list "%s": %s', listname, e) return # Set the language for the list @@ -99,8 +99,9 @@ def main(): %(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. + %(days)s days after the initial request. They also expire if the + request has already been handled in some way. If your confirmation + has expired, please try to re-submit your request. Otherwise, <a href="%(confirmurl)s">re-enter</a> your confirmation string.''') @@ -258,7 +259,8 @@ def subscription_prompt(mlist, doc, cookie, userdesc): <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): + if (mlist.subscribe_policy in (2, 3) and + not getattr(userdesc, 'invitation', False)): # Confirmation is required result = _("""Your confirmation is required in order to continue with the subscription request to the mailing list <em>%(listname)s</em>. diff --git a/Mailman/Cgi/edithtml.py b/Mailman/Cgi/edithtml.py index ee1ccd04..d01ff889 100644 --- a/Mailman/Cgi/edithtml.py +++ b/Mailman/Cgi/edithtml.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -72,7 +72,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' print doc.Format() - syslog('error', 'No such list "%s": %s', listname, e) + syslog('error', 'edithtml: No such list "%s": %s', listname, e) return # Now that we have a valid list, set the language to its default diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py index 8aaae14c..c13fdb26 100644 --- a/Mailman/Cgi/listinfo.py +++ b/Mailman/Cgi/listinfo.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2010 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -22,6 +22,7 @@ import os import cgi +import time from Mailman import mm_cfg from Mailman import Utils @@ -52,7 +53,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' listinfo_overview(_('No such list <em>%(safelistname)s</em>')) - syslog('error', 'No such list "%s": %s', listname, e) + syslog('error', 'listinfo: No such list "%s": %s', listname, e) return # See if the user want to see this page in other language @@ -184,6 +185,30 @@ def list_listinfo(mlist, lang): replacements['<mm-confirm-password>'] = mlist.FormatSecureBox('pw-conf') replacements['<mm-subscribe-form-start>'] = mlist.FormatFormStart( 'subscribe') + if mm_cfg.SUBSCRIBE_FORM_SECRET: + now = str(int(time.time())) + remote = os.environ.get('HTTP_FORWARDED_FOR', + os.environ.get('HTTP_X_FORWARDED_FOR', + os.environ.get('REMOTE_ADDR', + 'w.x.y.z'))) + # Try to accept a range in case of load balancers, etc. (LP: #1447445) + if remote.find('.') >= 0: + # ipv4 - drop last octet + remote = remote.rsplit('.', 1)[0] + else: + # ipv6 - drop last 16 (could end with :: in which case we just + # drop one : resulting in an invalid format, but it's only + # for our hash so it doesn't matter. + remote = remote.rsplit(':', 1)[0] + replacements['<mm-subscribe-form-start>'] += ( + '<input type="hidden" name="sub_form_token" value="%s:%s">\n' + % (now, Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET + + now + + mlist.internal_name() + + remote + ).hexdigest() + ) + ) # Roster form substitutions replacements['<mm-roster-form-start>'] = mlist.FormatFormStart('roster') replacements['<mm-roster-option>'] = mlist.FormatRosterOptionForUser(lang) diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py index 9a2389a9..cdc2bef3 100644 --- a/Mailman/Cgi/options.py +++ b/Mailman/Cgi/options.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,6 +17,7 @@ """Produce and handle the member options.""" +import re import sys import os import cgi @@ -33,8 +34,12 @@ from Mailman import i18n from Mailman.htmlformat import * from Mailman.Logging.Syslog import syslog +OR = '|' SLASH = '/' SETLANGUAGE = -1 +DIGRE = re.compile( + '<!--Start-Digests-Delete-->.*<!--End-Digests-Delete-->', + re.DOTALL) # Set up i18n _ = i18n._ @@ -52,6 +57,18 @@ def main(): doc = Document() doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) + method = Utils.GetRequestMethod() + if method.lower() not in ('get', 'post'): + title = _('CGI script error') + doc.SetTitle(title) + doc.AddItem(Header(2, title)) + doc.addError(_('Invalid request method: %(method)s')) + doc.AddItem('<hr>') + doc.AddItem(MailmanLogo()) + print 'Status: 405 Method Not Allowed' + print doc.Format() + return + parts = Utils.GetPathPieces() lenparts = parts and len(parts) if not parts or lenparts < 1: @@ -81,7 +98,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' print doc.Format() - syslog('error', 'No such list "%s": %s\n', listname, e) + syslog('error', 'options: No such list "%s": %s\n', listname, e) return # The total contents of the user's response @@ -112,6 +129,14 @@ def main(): return else: user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:]))) + # If a user submits a form or URL with post data or query fragments + # with multiple occurrences of the same variable, we can get a list + # here. Be as careful as possible. + if isinstance(user, list) or isinstance(user, tuple): + if len(user) == 0: + user = '' + else: + user = user[-1] # Avoid cross-site scripting attacks safeuser = Utils.websafe(user) @@ -164,6 +189,9 @@ def main(): return # Are we processing an unsubscription request from the login screen? + msgc = _('If you are a list member, a confirmation email has been sent.') + msga = _("""If you are a list member, your unsubscription request has been + forwarded to the list administrator for approval.""") if cgidata.has_key('login-unsub'): # Because they can't supply a password for unsubscribing, we'll need # to do the confirmation dance. @@ -175,14 +203,14 @@ def main(): # 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='') + doc.addError(msga, tag='') else: - ip = os.environ.get('REMOTE_ADDR') + ip = os.environ.get('HTTP_FORWARDED_FOR', + os.environ.get('HTTP_X_FORWARDED_FOR', + os.environ.get('REMOTE_ADDR', + 'unidentified origin'))) mlist.ConfirmUnsubscription(user, userlang, remote=ip) - doc.addError(_('The confirmation email has been sent.'), - tag='') + doc.addError(msgc, tag='') mlist.Save() finally: mlist.Unlock() @@ -195,19 +223,21 @@ def main(): syslog('mischief', 'Unsub attempt of non-member w/ private rosters: %s', user) - doc.addError(_('The confirmation email has been sent.'), - tag='') + if mlist.unsubscribe_policy: + doc.addError(msga, tag='') + else: + doc.addError(msgc, tag='') loginpage(mlist, doc, user, language) print doc.Format() return # Are we processing a password reminder from the login screen? + msg = _("""If you are a list member, + your password has been emailed to you.""") 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='') + doc.addError(msg, tag='') else: # Not a member if mlist.private_roster == 0: @@ -217,9 +247,7 @@ def main(): syslog('mischief', 'Reminder attempt of non-member w/ private rosters: %s', user) - doc.addError( - _('A reminder of your password has been emailed to you.'), - tag='') + doc.addError(msg, tag='') loginpage(mlist, doc, user, language) print doc.Format() return @@ -251,9 +279,13 @@ def main(): # So as not to allow membership leakage, prompt for the email # address and the password here. if mlist.private_roster <> 0: + remote = os.environ.get('HTTP_FORWARDED_FOR', + os.environ.get('HTTP_X_FORWARDED_FOR', + os.environ.get('REMOTE_ADDR', + 'unidentified origin'))) syslog('mischief', - 'Login failure with private rosters: %s', - user) + 'Login failure with private rosters: %s from %s', + user, remote) user = None # give an HTTP 401 for authentication failure print 'Status: 401 Unauthorized' @@ -265,6 +297,14 @@ def main(): # options. The first set of checks does not require the list to be # locked. + # However, if a form is submitted for a user who has been asynchronously + # unsubscribed, uncaught NotAMemberError exceptions can be thrown. + + if not mlist.isMember(user): + loginpage(mlist, doc, user, language) + print doc.Format() + return + if cgidata.has_key('logout'): print mlist.ZapCookie(mm_cfg.AuthUser, user) loginpage(mlist, doc, user, language) @@ -506,6 +546,13 @@ address. Upon confirmation, any other mailing list containing the address user, 'via the member options page', userack=1) except Errors.MMNeedApproval: needapproval = True + except Errors.NotAMemberError: + # MAS This except should really be in the outer try so we + # don't save the list redundantly, but except and finally in + # the same try requires Python >= 2.5. + # Setting a switch and making the Save() conditional doesn't + # seem worth it as the Save() won't change anything. + pass mlist.Save() finally: mlist.Unlock() @@ -846,8 +893,10 @@ You are subscribed to this list with the case-preserved address else: replacements['<mm-case-preserved-user>'] = '' - doc.AddItem(mlist.ParseTags('options.html', replacements, userlang)) - + page_text = mlist.ParseTags('options.html', replacements, userlang) + if not (mlist.digestable or mlist.getMemberOption(user, mm_cfg.Digests)): + page_text = DIGRE.sub('', page_text) + doc.AddItem(page_text) def loginpage(mlist, doc, user, lang): @@ -1049,7 +1098,8 @@ def topic_details(mlist, doc, user, cpuser, userlang, varhelp): table.AddRow([Bold(Label(_('Name:'))), Utils.websafe(name)]) table.AddRow([Bold(Label(_('Pattern (as regexp):'))), - '<pre>' + Utils.websafe(pattern) + '</pre>']) + '<pre>' + Utils.websafe(OR.join(pattern.splitlines())) + + '</pre>']) table.AddRow([Bold(Label(_('Description:'))), Utils.websafe(description)]) # Make colors look nice diff --git a/Mailman/Cgi/private.py b/Mailman/Cgi/private.py index b0358285..36cacee4 100644..100755 --- a/Mailman/Cgi/private.py +++ b/Mailman/Cgi/private.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -111,7 +111,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' print doc.Format() - syslog('error', 'No such list "%s": %s\n', listname, e) + syslog('error', 'private: No such list "%s": %s\n', listname, e) return i18n.set_language(mlist.preferred_language) @@ -135,6 +135,27 @@ def main(): message = Bold(FontSize('+1', _('Authorization failed.'))).Format() # give an HTTP 401 for authentication failure print 'Status: 401 Unauthorized' + # Are we processing a password reminder from the login screen? + if cgidata.has_key('login-remind'): + if username: + message = Bold(FontSize('+1', _("""If you are a list member, + your password has been emailed to you."""))).Format() + else: + message = Bold(FontSize('+1', + _('Please enter your email address'))).Format() + if mlist.isMember(username): + mlist.MailUserPassword(username) + elif username: + # Not a member + if mlist.private_roster == 0: + # Public rosters + safeuser = Utils.websafe(username) + message = Bold(FontSize('+1', + _('No such member: %(safeuser)s.'))).Format() + else: + syslog('mischief', + 'Reminder attempt of non-member w/ private rosters: %s', + username) # Output the password form charset = Utils.GetCharSet(mlist.preferred_language) print 'Content-type: text/html; charset=' + charset + '\n\n' diff --git a/Mailman/Cgi/rmlist.py b/Mailman/Cgi/rmlist.py index 8988dc42..db588121 100644 --- a/Mailman/Cgi/rmlist.py +++ b/Mailman/Cgi/rmlist.py @@ -1,4 +1,4 @@ -# Copyright (C) 2001-2010 by the Free Software Foundation, Inc. +# Copyright (C) 2001-2014 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -62,7 +62,7 @@ def main(): # Avoid cross-site scripting attacks safelistname = Utils.websafe(listname) title = _('No such list <em>%(safelistname)s</em>') - doc.SetTitle(title) + doc.SetTitle(_('No such list %(safelistname)s')) doc.AddItem( Header(3, Bold(FontAttr(title, color='#ff0000', size='+2')))) @@ -71,7 +71,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' print doc.Format() - syslog('error', 'No such list "%s": %s\n', listname, e) + syslog('error', 'rmlist: No such list "%s": %s\n', listname, e) return # Now that we have a valid mailing list, set the language @@ -188,7 +188,7 @@ def process_request(doc, cgidata, mlist): def request_deletion(doc, mlist, errmsg=None): realname = mlist.real_name title = _('Permanently remove mailing list <em>%(realname)s</em>') - doc.SetTitle(title) + doc.SetTitle(_('Permanently remove mailing list %(realname)s')) table = Table(border=0, width='100%') table.AddRow([Center(Bold(FontAttr(title, size='+1')))]) diff --git a/Mailman/Cgi/roster.py b/Mailman/Cgi/roster.py index 6260c973..6c64925b 100644 --- a/Mailman/Cgi/roster.py +++ b/Mailman/Cgi/roster.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2014 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -57,7 +57,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' error_page(_('No such list <em>%(safelistname)s</em>')) - syslog('error', 'roster: no such list "%s": %s', listname, e) + syslog('error', 'roster: No such list "%s": %s', listname, e) return cgidata = cgi.FieldStorage() diff --git a/Mailman/Cgi/subscribe.py b/Mailman/Cgi/subscribe.py index 7c49c51c..ab5c7cd8 100644..100755 --- a/Mailman/Cgi/subscribe.py +++ b/Mailman/Cgi/subscribe.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2011 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2015 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,6 +20,7 @@ import sys import os import cgi +import time import signal from Mailman import mm_cfg @@ -63,7 +64,7 @@ def main(): # Send this with a 404 status. print 'Status: 404 Not Found' print doc.Format() - syslog('error', 'No such list "%s": %s\n', listname, e) + syslog('error', 'subscribe: No such list "%s": %s\n', listname, e) return # See if the form data has a preferred language set, in which case, use it @@ -117,9 +118,44 @@ def process_form(mlist, doc, cgidata, lang): # 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')) + remote = os.environ.get('HTTP_FORWARDED_FOR', + os.environ.get('HTTP_X_FORWARDED_FOR', + os.environ.get('REMOTE_ADDR', + 'unidentified origin'))) + # Are we checking the hidden data? + if mm_cfg.SUBSCRIBE_FORM_SECRET: + now = int(time.time()) + # Try to accept a range in case of load balancers, etc. (LP: #1447445) + if remote.find('.') >= 0: + # ipv4 - drop last octet + remote1 = remote.rsplit('.', 1)[0] + else: + # ipv6 - drop last 16 (could end with :: in which case we just + # drop one : resulting in an invalid format, but it's only + # for our hash so it doesn't matter. + remote1 = remote.rsplit(':', 1)[0] + try: + ftime, fhash = cgidata.getvalue('sub_form_token', '').split(':') + then = int(ftime) + except ValueError: + ftime = fhash = '' + then = 0 + token = Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET + + ftime + + mlist.internal_name() + + remote1).hexdigest() + if ftime and now - then > mm_cfg.FORM_LIFETIME: + results.append(_('The form is too old. Please GET it again.')) + if ftime and now - then < mm_cfg.SUBSCRIBE_FORM_MIN_TIME: + results.append( + _('Please take a few seconds to fill out the form before submitting it.')) + if ftime and token != fhash: + results.append( + _("The hidden token didn't match. Did your IP change?")) + if not ftime: + results.append( + _('There was no hidden token in your submission or it was corrupted.')) + results.append(_('You must GET the form before submitting it.')) # Was an attempt made to subscribe the list to itself? if email == mlist.GetListEmail(): syslog('mischief', 'Attempt to self subscribe %s: %s', email, remote) |