aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Cgi
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/Cgi')
-rw-r--r--Mailman/Cgi/admin.py150
-rw-r--r--Mailman/Cgi/admindb.py189
-rw-r--r--Mailman/Cgi/confirm.py12
-rw-r--r--Mailman/Cgi/edithtml.py4
-rw-r--r--Mailman/Cgi/listinfo.py29
-rw-r--r--Mailman/Cgi/options.py92
-rwxr-xr-x[-rw-r--r--]Mailman/Cgi/private.py25
-rw-r--r--Mailman/Cgi/rmlist.py8
-rw-r--r--Mailman/Cgi/roster.py4
-rwxr-xr-x[-rw-r--r--]Mailman/Cgi/subscribe.py46
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() +
+ '&nbsp;' +
+ _('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() +
+ '&nbsp;' +
+ _('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() +
'&nbsp;' +
- _('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() +
'&nbsp;' +
- _('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() + \
- '&nbsp;' + _('Permanently ban from this list')
+ radio += ('<br>' + '<label>' +
+ CheckBox('ban-%d' % id, 1).Format() +
+ '&nbsp;' + _('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() +
'&nbsp;' +
- _('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() +
'&nbsp;' +
- _('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() +
'&nbsp;' +
- _("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() +
'&nbsp;' +
- _('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() +
'&nbsp;' +
_("""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(['&nbsp;', '&nbsp;'])
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(['&nbsp;',
+ '<label>' +
CheckBox('preserve-%d' % id, 'on', 0).Format() +
- '&nbsp;' + _('Preserve message for site administrator')
+ '&nbsp;' + _('Preserve message for site administrator') +
+ '</label>'
])
t.AddRow(['&nbsp;',
+ '<label>' +
CheckBox('forward-%d' % id, 'on', 0).Format() +
'&nbsp;' + _('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)