Merge lp:~jimpop/mailman/mailman-auto-mod-verbose-members into lp:mailman/2.1

Proposed by Jim Popovitch
Status: Merged
Merged at revision: 1595
Proposed branch: lp:~jimpop/mailman/mailman-auto-mod-verbose-members
Merge into: lp:mailman/2.1
Diff against target: 120 lines (+50/-1)
6 files modified
Mailman/Defaults.py.in (+6/-0)
Mailman/Gui/Privacy.py (+12/-0)
Mailman/Handlers/SpamDetect.py (+8/-0)
Mailman/Utils.py (+19/-0)
Mailman/Version.py (+1/-1)
Mailman/versions.py (+4/-0)
To merge this branch: bzr merge lp:~jimpop/mailman/mailman-auto-mod-verbose-members
Reviewer Review Type Date Requested Status
Mark Sapiro Approve
Mark Shapiro Pending
Review via email: mp+276706@code.launchpad.net

Commit message

Auto-Moderate verbose members

Description of the change

This is a simple patch to enable the following two Privacy/Sender features:

  member_verbosity_threshold
     Ceiling on acceptable number of member posts, per interval, before automatic moderation.

  member_verbosity_interval
     Number of seconds to use in determining whether or not to automatically moderate a member.

The code tracks member posts in a simple python dictionary, and then checks the dictionary each time a post is processed via Mailman/Handlers/SpamDetect.py. If a member exceeds X posts in Y time, then the member is automatically moderated.

To post a comment you must log in.
1587. By <email address hidden>

Removed 2 development debugging lines

Revision history for this message
Mark Sapiro (msapiro) wrote :

It is not clear why you have

 add_only_if_missing('member_verbosity_threshold', 0)

in versions.py instead of

 add_only_if_missing('member_verbosity_threshold',
                     mm_cfg.DEFAULT_MEMBER_VERBOSITY_THRESHOLD)

Oversight perhaps. Also I think Defaults.py.in should probably have

DEFAULT_MEMBER_VERBOSITY_THRESHOLD = 0

Also, the first few lines of Utils.IsVerboseMember() seem unnecessary, and I'm not completely comfortable with recentMemberPostings being a global in Utils.py rather than a list attribute.

And, do you really see 5 posts from the same member in 5 minutes?

Finally, I'm curious if you ever saw <https://www.msapiro.net/scripts/PostLimit.py>. I guess it is very different in detail, but the goal may be similar.

I've been reluctant to actually implement that on my production server, although it has been suggested as a way to control one member (I'd rather have a conversation than hit him with a hammer, although hammers have their place.)

review: Needs Information
Revision history for this message
Jim Popovitch (jimpop) wrote :

Hi Mark, thank you. I have made the following improvements based on your feedback:

Mailman/versions.py:
    set mm_cfg.DEFAULT_MEMBER_VERBOSITY_THRESHOLD rather than hardcoded "0".

Mailman/Defaults.py.in:
    DEFAULT_MEMBER_VERBOSITY_THRESHOLD now defaults to 0

Mailman/Utils.py:
    Removed unnecessary value checks from the top of Utils.IsVerboseMember()

Mailman/Handlers/SpamDetect.py
    Added "mlist.member_verbosity_threshold > 0" test before call to Utils.IsVerboseMember()

I set recentMemberPostings as a global because I felt it was important to measure a members posting pattern across all lists, not a single list. Although I currently only moderated the user on the single impacted list, ideally I'd like to find a way to moderate the user on all subscribed lists.

NANOG recently had a problem of hundreds of spam posts in a matter of minutes. Search for "Fw: new messsage" on http://mailman.nanog.org/pipermail/nanog/2015-October/thread.html (DO NOT CLICK ON ANY OF THE EMAILED LINKS IN THOSE NANOG POSTS). There were so many posts it clogged their outbound queues for at least 1 day.

I have not seen PostLimit before, thanks. I honestly don't care which version makes it into Mailman, but there is clear evidence that spamvertised postings, using a known member address, is going to be a huge problem going forward.

1588. By <email address hidden>

Improvements based on feedback from Mark Sapiro
   https://code.launchpad.net/~jimpop/mailman/mailman-auto-mod-verbose-members/+merge/276706/comments/699744

Revision history for this message
Mark Sapiro (msapiro) wrote :

Thanks for your replies and changes. I now have a much clearer understanding of the issue that prompts this. I will probably do a little tweaking, but it looks good and I will get it in the next release.

It really has different goals from PostLimit which I did as a response to some request on Mailman-users.

Actually, we could have used something like this for a flood of bogus subscription requests a while back, but the addresses were predictable enough that I was able to handle it with a regexp in the new GLOBAL_BAN_LIST (and variable enough that matching on the address wouldn't work).

Anyway, You've done good work here. Thank you. I will get the feature in the next release (and probably aim for a release soon, but not before December as I a traveling the next 3 weeks)

review: Approve
Revision history for this message
Mark Sapiro (msapiro) wrote :

I have merged this branch and made some changes. The most significant is your code to drop old times from the recentMemberPostings dictionary is wrong. Deleting items from a list while processing 'for item in list' results in skipping items.

I also allow for different lists to have different member_verbosity_interval settings and process that I hope reasonably.

Finally, I don't attempt to moderate a non-member, and I look at more than the From: of the message to find a member address.

Revision history for this message
Jim Popovitch (jimpop) wrote :

Sounds good Mark, Thanks

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Mailman/Defaults.py.in'
2--- Mailman/Defaults.py.in 2015-09-19 02:18:34 +0000
3+++ Mailman/Defaults.py.in 2015-11-05 00:11:03 +0000
4@@ -1118,6 +1118,12 @@
5 # The total time to spend trying to get an answer to the question.
6 DMARC_RESOLVER_LIFETIME = seconds(5)
7
8+# Should the list server auto-moderate members who post too frequently
9+# DEFAULT_MEMBER_VERBOSITY_INTERVAL = number of seconds to track posts
10+# DEFAULT_MEMBER_VERBOSITY_THRESHOLD = number of allowed posts per interval (0 to disable).
11+DEFAULT_MEMBER_VERBOSITY_INTERVAL = 300
12+DEFAULT_MEMBER_VERBOSITY_THRESHOLD = 0
13+
14 # What domains should be considered equivalent when testing list membership
15 # for posting/moderation.
16 # If two poster addresses with the same local part but
17
18=== modified file 'Mailman/Gui/Privacy.py'
19--- Mailman/Gui/Privacy.py 2015-02-04 04:48:25 +0000
20+++ Mailman/Gui/Privacy.py 2015-11-05 00:11:03 +0000
21@@ -229,6 +229,18 @@
22 <a href="%(adminurl)s/members">membership management
23 screens</a>.""")),
24
25+ ('member_verbosity_threshold', mm_cfg.Number, 5, 0,
26+ _('Ceiling on acceptable number of member posts, per interval, before automatic moderation.'),
27+
28+ _('''If a member posts this many times, within sender_verbosity_interval,
29+ the member is automatically moderated. Use 0 to disable.''')),
30+
31+ ('member_verbosity_interval', mm_cfg.Number, 5, 300,
32+ _('Number of seconds to use in determining whether or not to automatically moderate a member.'),
33+
34+ _('''If a member posts exceed member_verbosity_threshold, within sender_verbosity_interval,
35+ the member is automatically moderated.''')),
36+
37 ('member_moderation_action', mm_cfg.Radio,
38 (_('Hold'), _('Reject'), _('Discard')), 0,
39 _("""Action to take when a moderated member posts to the
40
41=== modified file 'Mailman/Handlers/SpamDetect.py'
42--- Mailman/Handlers/SpamDetect.py 2015-03-02 21:50:24 +0000
43+++ Mailman/Handlers/SpamDetect.py 2015-11-05 00:11:03 +0000
44@@ -122,6 +122,12 @@
45 raise Errors.RejectMessage, text
46 elif mlist.dmarc_moderation_action == 4:
47 raise Errors.DiscardMessage
48+
49+ if mlist.member_verbosity_threshold > 0 and Utils.IsVerboseMember(mlist, addr):
50+ mlist.setMemberOption(addr, mm_cfg.Moderate, 1)
51+ syslog('vette', '%s: Automatically Moderated %s for verbose postings.',
52+ mlist.real_name, addr)
53+
54 if msgdata.get('approved'):
55 return
56 # First do site hard coded header spam checks
57@@ -169,3 +175,5 @@
58 hold_for_approval(mlist, msg, msgdata, HeaderMatchHold)
59 if action == mm_cfg.ACCEPT:
60 return
61+
62+
63
64=== modified file 'Mailman/Utils.py'
65--- Mailman/Utils.py 2015-09-17 00:39:34 +0000
66+++ Mailman/Utils.py 2015-11-05 00:11:03 +0000
67@@ -1246,6 +1246,25 @@
68 return False
69
70
71+# Check a known list in order to auto-moderate verbose members
72+recentMemberPostings = {};
73+def IsVerboseMember(mlist, email):
74+
75+ if mlist.member_verbosity_threshold == 0:
76+ return False
77+
78+ email = email.lower()
79+
80+ t = time.time()
81+ recentMemberPostings.setdefault(email,[]).append(t)
82+
83+ for t in recentMemberPostings[email]:
84+ if t < time.time() - float(mlist.member_verbosity_interval):
85+ recentMemberPostings[email].remove(t)
86+
87+ return len(recentMemberPostings[email]) >= mlist.member_verbosity_threshold
88+
89+
90 def check_eq_domains(email, domains_list):
91 """The arguments are an email address and a string representing a
92 list of lists in a form like 'a,b,c;1,2' representing [['a', 'b',
93
94=== modified file 'Mailman/Version.py'
95--- Mailman/Version.py 2015-03-31 17:18:39 +0000
96+++ Mailman/Version.py 2015-11-05 00:11:03 +0000
97@@ -37,7 +37,7 @@
98 (REL_LEVEL << 4) | (REL_SERIAL << 0))
99
100 # config.pck schema version number
101-DATA_FILE_VERSION = 108
102+DATA_FILE_VERSION = 109
103
104 # qfile/*.db schema version number
105 QFILE_SCHEMA_VERSION = 3
106
107=== modified file 'Mailman/versions.py'
108--- Mailman/versions.py 2015-02-08 07:54:26 +0000
109+++ Mailman/versions.py 2015-11-05 00:11:03 +0000
110@@ -497,6 +497,10 @@
111 add_only_if_missing('dmarc_moderation_notice', '')
112 add_only_if_missing('dmarc_wrapped_message_text',
113 mm_cfg.DEFAULT_DMARC_WRAPPED_MESSAGE_TEXT)
114+ add_only_if_missing('member_verbosity_threshold',
115+ mm_cfg.DEFAULT_MEMBER_VERBOSITY_THRESHOLD)
116+ add_only_if_missing('member_verbosity_interval',
117+ mm_cfg.DEFAULT_MEMBER_VERBOSITY_INTERVAL)
118 add_only_if_missing('equivalent_domains',
119 mm_cfg.DEFAULT_EQUIVALENT_DOMAINS)
120 add_only_if_missing('new_member_options',