diff options
Diffstat (limited to '')
-rw-r--r-- | Mailman/Cgi/options.py | 8 | ||||
-rwxr-xr-x | Mailman/Defaults.py.in | 15 | ||||
-rw-r--r-- | Mailman/Gui/General.py | 4 | ||||
-rw-r--r-- | Mailman/Gui/Privacy.py | 55 | ||||
-rwxr-xr-x | Mailman/Handlers/CalcRecips.py | 6 | ||||
-rwxr-xr-x | Mailman/Handlers/CookHeaders.py | 5 | ||||
-rw-r--r-- | Mailman/Handlers/SpamDetect.py | 21 | ||||
-rwxr-xr-x | Mailman/MailList.py | 8 | ||||
-rw-r--r-- | Mailman/Queue/IncomingRunner.py | 10 | ||||
-rw-r--r-- | Mailman/Queue/NewsRunner.py | 22 | ||||
-rw-r--r-- | Mailman/Utils.py | 48 | ||||
-rw-r--r-- | Mailman/Version.py | 2 | ||||
-rwxr-xr-x | Mailman/versions.py | 2 |
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 |