aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--Mailman/Cgi/options.py8
-rwxr-xr-xMailman/Defaults.py.in15
-rw-r--r--Mailman/Gui/General.py4
-rw-r--r--Mailman/Gui/Privacy.py55
-rwxr-xr-xMailman/Handlers/CalcRecips.py6
-rwxr-xr-xMailman/Handlers/CookHeaders.py5
-rw-r--r--Mailman/Handlers/SpamDetect.py21
-rwxr-xr-xMailman/MailList.py8
-rw-r--r--Mailman/Queue/IncomingRunner.py10
-rw-r--r--Mailman/Queue/NewsRunner.py22
-rw-r--r--Mailman/Utils.py48
-rw-r--r--Mailman/Version.py2
-rwxr-xr-xMailman/versions.py2
13 files changed, 148 insertions, 58 deletions
diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py
index 7b2c70b5..cdc2bef3 100644
--- a/Mailman/Cgi/options.py
+++ b/Mailman/Cgi/options.py
@@ -297,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)
diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in
index 80d8694c..a4b63285 100755
--- a/Mailman/Defaults.py.in
+++ b/Mailman/Defaults.py.in
@@ -1119,10 +1119,23 @@ DMARC_RESOLVER_TIMEOUT = seconds(3)
DMARC_RESOLVER_LIFETIME = seconds(5)
# Should the list server auto-moderate members who post too frequently
+# This is intended to stop people who join a list and then use a bot to
+# send many spam messages in a short interval. These are default settings
+# for new lists. See the web admin Privacy options -> Sender filters page
+# and the Details for member_verbosity_threshold and member_verbosity_interval
+# links for more information.
# DEFAULT_MEMBER_VERBOSITY_INTERVAL = number of seconds to track posts
-# DEFAULT_MEMBER_VERBOSITY_THRESHOLD = number of allowed posts per interval (0 to disable).
+# DEFAULT_MEMBER_VERBOSITY_THRESHOLD = number of allowed posts per interval
+# (0 to disable).
DEFAULT_MEMBER_VERBOSITY_INTERVAL = 300
DEFAULT_MEMBER_VERBOSITY_THRESHOLD = 0
+
+# This controls how often to clean old post time entries from the dictionary
+# used to implement the member verbosity feature. This is a compromise between
+# using resources for cleaning and allowing the dictionary to grow very large.
+# The setting is the number of passes through the code before the dictionary
+# is cleaned.
+VERBOSE_CLEAN_LIMIT = 1000
# What domains should be considered equivalent when testing list membership
# for posting/moderation.
diff --git a/Mailman/Gui/General.py b/Mailman/Gui/General.py
index 4b67f446..980e5f2b 100644
--- a/Mailman/Gui/General.py
+++ b/Mailman/Gui/General.py
@@ -255,7 +255,7 @@ class General(GUIBase):
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'
+ href="http://marc.merlins.org/netrants/reply-to-harmful.html">`Reply-To'
Munging Considered Harmful</a> for a general discussion of this
issue. See <a
href="http://marc.merlins.org/netrants/reply-to-useful.html">
@@ -283,7 +283,7 @@ class General(GUIBase):
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'
+ href="http://marc.merlins.org/netrants/reply-to-harmful.html">`Reply-To'
Munging Considered Harmful</a> for a general discussion of this
issue. See <a
href="http://marc.merlins.org/netrants/reply-to-useful.html">
diff --git a/Mailman/Gui/Privacy.py b/Mailman/Gui/Privacy.py
index 68f508af..a9ce0cde 100644
--- a/Mailman/Gui/Privacy.py
+++ b/Mailman/Gui/Privacy.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2001-2015 by the Free Software Foundation, Inc.
+# Copyright (C) 2001-2016 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
@@ -61,7 +61,7 @@ class Privacy(GUIBase):
_('Confirm and approve')),
0,
_('What steps are required for subscription?<br>'),
- _('''None - no verification steps (<em>Not
+ _("""None - no verification steps (<em>Not
Recommended </em>)<br>
Confirm (*) - email confirmation step required <br>
Require approval - require list administrator
@@ -75,7 +75,7 @@ class Privacy(GUIBase):
This prevents mischievous (or malicious) people
from creating subscriptions for others without
- their consent.'''))
+ their consent."""))
else:
sub_cfentry = ('subscribe_policy', mm_cfg.Radio,
# choices
@@ -84,7 +84,7 @@ class Privacy(GUIBase):
_('Confirm and approve')),
1,
_('What steps are required for subscription?<br>'),
- _('''Confirm (*) - email confirmation required <br>
+ _("""Confirm (*) - email confirmation required <br>
Require approval - require list administrator
approval for subscriptions <br>
Confirm and approve - both confirm and approve
@@ -94,7 +94,7 @@ class Privacy(GUIBase):
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.'''))
+ subscriptions for others without their consent."""))
# some helpful values
admin = mlist.GetScriptURL('admin')
@@ -108,8 +108,8 @@ class Privacy(GUIBase):
_('Subscribing'),
('advertised', mm_cfg.Radio, (_('No'), _('Yes')), 0,
- _('''Advertise this list when people ask what lists are on this
- machine?''')),
+ _("""Advertise this list when people ask what lists are on this
+ machine?""")),
sub_cfentry,
@@ -155,8 +155,8 @@ class Privacy(GUIBase):
(_('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.''')),
+ _("""When set, the list of subscribers is protected by member or
+ admin password authentication.""")),
('obscure_addresses', mm_cfg.Radio, (_('No'), _('Yes')), 0,
_("""Show member addresses so they're not directly recognizable
@@ -230,16 +230,35 @@ class Privacy(GUIBase):
screens</a>.""")),
('member_verbosity_threshold', mm_cfg.Number, 5, 0,
- _('Ceiling on acceptable number of member posts, per interval, before automatic moderation.'),
+ _("""Ceiling on acceptable number of member posts, per interval,
+ before automatic moderation."""),
- _('''If a member posts this many times, within sender_verbosity_interval,
- the member is automatically moderated. Use 0 to disable.''')),
+ _("""If a member posts this many times, within a period of time
+ the member is automatically moderated. Use 0 to disable. See
+ <a href="?VARHELP=privacy/sender/member_verbosity_interval"
+ >member_verbosity_interval</a> for details on the time period.
- ('member_verbosity_interval', mm_cfg.Number, 5, 300,
- _('Number of seconds to use in determining whether or not to automatically moderate a member.'),
+ <p>This is intended to stop people who join a list or lists and
+ then use a bot to send many spam messages in a short interval.
- _('''If a member posts exceed member_verbosity_threshold, within sender_verbosity_interval,
- the member is automatically moderated.''')),
+ <p>Be careful when using this setting. If it is set too low,
+ this can be triggered by a single post cross-posted to
+ multiple lists or by a single post to an umbrella list.""")),
+
+ ('member_verbosity_interval', mm_cfg.Number, 5, 0,
+ _("""Number of seconds to keep posts to this list to determine
+ member_verbosity_threshold for automatic moderation of a
+ member."""),
+
+ _("""If a member's total posts to all lists in this installation
+ with member_verbosity_threshold enabled exceeds this list's
+ member_verbosity_threshold, the member is automatically
+ moderated on this list.
+
+ <p>Posts which are counted towards this list's
+ member_verbosity_threshold are all posts to any list with
+ member_verbosity_threshold enabled that arrived within that
+ list's member_verbosity_interval.""")),
('member_moderation_action', mm_cfg.Radio,
(_('Hold'), _('Reject'), _('Discard')), 0,
@@ -496,8 +515,8 @@ class Privacy(GUIBase):
('max_num_recipients', mm_cfg.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.''')),
+ _("""If a posting has this number, or more, of recipients, it is
+ held for admin approval. Use 0 for no ceiling.""")),
]
spam_rtn = [
diff --git a/Mailman/Handlers/CalcRecips.py b/Mailman/Handlers/CalcRecips.py
index 0680bfb1..069c88a8 100755
--- a/Mailman/Handlers/CalcRecips.py
+++ b/Mailman/Handlers/CalcRecips.py
@@ -184,6 +184,12 @@ def do_exclude(mlist, msg, msgdata, recips):
for sender in msg.get_senders():
if slist.isMember(sender):
break
+ for sender in Utils.check_eq_domains(sender,
+ slist.equivalent_domains):
+ if slist.isMember(sender):
+ break
+ if slist.isMember(sender):
+ break
else:
continue
srecips = set([slist.getMemberCPAddress(m)
diff --git a/Mailman/Handlers/CookHeaders.py b/Mailman/Handlers/CookHeaders.py
index acb79166..4e005dc5 100755
--- a/Mailman/Handlers/CookHeaders.py
+++ b/Mailman/Handlers/CookHeaders.py
@@ -425,6 +425,11 @@ def prefix_subject(mlist, msg, msgdata):
except UnicodeError:
pass
# Get the header as a Header instance, with proper unicode conversion
+ # Because of rfc2047 encoding, spaces between encoded words can be
+ # insignificant, so we need to append a space to prefix but only when
+ # we have Re:.
+ if recolon:
+ prefix += ' '
if old_style:
h = uheader(mlist, recolon, 'Subject', continuation_ws=ws)
h.append(prefix)
diff --git a/Mailman/Handlers/SpamDetect.py b/Mailman/Handlers/SpamDetect.py
index 509e1e6e..c7b8d9e7 100644
--- a/Mailman/Handlers/SpamDetect.py
+++ b/Mailman/Handlers/SpamDetect.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2015 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2016 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
@@ -123,10 +123,19 @@ error, contact the mailing list owner at %(listowner)s."""))
elif mlist.dmarc_moderation_action == 4:
raise Errors.DiscardMessage
- if mlist.member_verbosity_threshold > 0 and Utils.IsVerboseMember(mlist, addr):
- mlist.setMemberOption(addr, mm_cfg.Moderate, 1)
- syslog('vette', '%s: Automatically Moderated %s for verbose postings.',
- mlist.real_name, addr)
+ # Get member address if any.
+ for sender in msg.get_senders():
+ if mlist.isMember(sender):
+ break
+ else:
+ sender = msg.get_sender()
+ if (mlist.member_verbosity_threshold > 0 and
+ Utils.IsVerboseMember(mlist, sender)
+ ):
+ mlist.setMemberOption(sender, mm_cfg.Moderate, 1)
+ syslog('vette',
+ '%s: Automatically Moderated %s for verbose postings.',
+ mlist.real_name, sender)
if msgdata.get('approved'):
return
@@ -175,5 +184,3 @@ error, contact the mailing list owner at %(listowner)s."""))
hold_for_approval(mlist, msg, msgdata, HeaderMatchHold)
if action == mm_cfg.ACCEPT:
return
-
-
diff --git a/Mailman/MailList.py b/Mailman/MailList.py
index b76dfa1f..0d47d812 100755
--- a/Mailman/MailList.py
+++ b/Mailman/MailList.py
@@ -883,8 +883,12 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin,
# Is the subscribing address banned from this list?
pattern = self.GetBannedPattern(email)
if pattern:
- syslog('vette', '%s banned subscription: %s (matched: %s)',
- realname, email, pattern)
+ if remote:
+ whence = ' from %s' % remote
+ else:
+ whence = ''
+ syslog('vette', '%s banned subscription: %s%s (matched: %s)',
+ realname, email, whence, pattern)
raise Errors.MembershipIsBanned, pattern
# Sanity check the digest flag
if digest and not self.digestable:
diff --git a/Mailman/Queue/IncomingRunner.py b/Mailman/Queue/IncomingRunner.py
index eab4d2c5..2c6c2815 100644
--- a/Mailman/Queue/IncomingRunner.py
+++ b/Mailman/Queue/IncomingRunner.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2014 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
@@ -157,6 +157,10 @@ class IncomingRunner(Runner):
os._exit(1)
except Errors.DiscardMessage:
# Throw the message away; we need do nothing else with it.
+ # We do need to push the current handler back in the pipeline
+ # just in case the syslog call throws an exception and the
+ # message is shunted.
+ pipeline.insert(0, handler)
syslog('vette', """Message discarded, msgid: %s'
list: %s,
handler: %s""",
@@ -169,6 +173,10 @@ class IncomingRunner(Runner):
return 0
except Errors.RejectMessage, e:
# Log this.
+ # We do need to push the current handler back in the pipeline
+ # just in case the syslog call or BounceMessage throws an
+ # exception and the message is shunted.
+ pipeline.insert(0, handler)
syslog('vette', """Message rejected, msgid: %s
list: %s,
handler: %s,
diff --git a/Mailman/Queue/NewsRunner.py b/Mailman/Queue/NewsRunner.py
index 44850063..449532fb 100644
--- a/Mailman/Queue/NewsRunner.py
+++ b/Mailman/Queue/NewsRunner.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2000-2005 by the Free Software Foundation, Inc.
+# Copyright (C) 2000-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
@@ -111,20 +111,12 @@ def prepare_message(mlist, msg, msgdata):
del msg['subject']
msg['subject'] = stripped_subject
# Add the appropriate Newsgroups: header
- ngheader = msg['newsgroups']
- if ngheader is not None:
- # See if the Newsgroups: header already contains our linked_newsgroup.
- # If so, don't add it again. If not, append our linked_newsgroup to
- # the end of the header list
- ngroups = [s.strip() for s in ngheader.split(',')]
- if mlist.linked_newsgroup not in ngroups:
- ngroups.append(mlist.linked_newsgroup)
- # Subtitute our new header for the old one.
- del msg['newsgroups']
- msg['Newsgroups'] = COMMASPACE.join(ngroups)
- else:
- # Newsgroups: isn't in the message
- msg['Newsgroups'] = mlist.linked_newsgroup
+ if msg['newsgroups'] is not None:
+ # This message is gated from our list to it's associated usnet group.
+ # If it has a Newsgroups: header mentioning other groups, it's not
+ # up to us to post it to those groups.
+ del msg['newsgroups']
+ msg['Newsgroups'] = mlist.linked_newsgroup
# Note: We need to be sure two messages aren't ever sent to the same list
# in the same process, since message ids need to be unique. Further, if
# messages are crossposted to two Usenet-gated mailing lists, they each
diff --git a/Mailman/Utils.py b/Mailman/Utils.py
index 70ef34e8..2404c445 100644
--- a/Mailman/Utils.py
+++ b/Mailman/Utils.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2015 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2016 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
@@ -1247,22 +1247,50 @@ def IsDMARCProhibited(mlist, email):
# Check a known list in order to auto-moderate verbose members
-recentMemberPostings = {};
+# dictionary to remember recent posts.
+recentMemberPostings = {}
+# counter of times through
+clean_count = 0
def IsVerboseMember(mlist, email):
+ """For lists that request it, we keep track of recent posts by address.
+A message from an address to a list, if the list requests it, is remembered
+for a specified time whether or not the address is a list member, and if the
+address is a member and the member is over the threshold for the list, that
+fact is returned."""
+
+ global clean_count
if mlist.member_verbosity_threshold == 0:
return False
email = email.lower()
- t = time.time()
- recentMemberPostings.setdefault(email,[]).append(t)
-
- for t in recentMemberPostings[email]:
- if t < time.time() - float(mlist.member_verbosity_interval):
- recentMemberPostings[email].remove(t)
-
- return len(recentMemberPostings[email]) >= mlist.member_verbosity_threshold
+ now = time.time()
+ recentMemberPostings.setdefault(email,[]).append(now +
+ float(mlist.member_verbosity_interval)
+ )
+ x = range(len(recentMemberPostings[email]))
+ x.reverse()
+ for i in x:
+ if recentMemberPostings[email][i] < now:
+ del recentMemberPostings[email][i]
+
+ clean_count += 1
+ if clean_count >= mm_cfg.VERBOSE_CLEAN_LIMIT:
+ clean_count = 0
+ for addr in recentMemberPostings.keys():
+ x = range(len(recentMemberPostings[addr]))
+ x.reverse()
+ for i in x:
+ if recentMemberPostings[addr][i] < now:
+ del recentMemberPostings[addr][i]
+ if not recentMemberPostings[addr]:
+ del recentMemberPostings[addr]
+ if not mlist.isMember(email):
+ return False
+ return (len(recentMemberPostings.get(email, [])) >=
+ mlist.member_verbosity_threshold
+ )
def check_eq_domains(email, domains_list):
diff --git a/Mailman/Version.py b/Mailman/Version.py
index 910cd55d..0695cd30 100644
--- a/Mailman/Version.py
+++ b/Mailman/Version.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2015 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2016 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
diff --git a/Mailman/versions.py b/Mailman/versions.py
index d13be94f..66775f05 100755
--- a/Mailman/versions.py
+++ b/Mailman/versions.py
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2015 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2016 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