[Mailman-Developers] Marc Perkel's Feature Wish List

Peter C. Norton spacey-mailman@lenin.nu
Wed, 2 Jan 2002 15:09:26 -0800


--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=iso-8859-1
Content-Disposition: inline
Content-Transfer-Encoding: 8bit

Well, attached is the patch.  If anyone who knows more can post the command
for modifying the database directly, then this would useable now.  Though
the interface to the same functionality will obviously change for 2.1.

-Peter

On Wed, Jan 02, 2002 at 02:29:09PM -0800, Dan Mick wrote:
> 
> > > I'd like a setting so that if non-members post to the list that their
> > > messages are automatically dropped (bounced) without list owner
> > > intervention. The bounce message should be able to be customized giving
> > > the user information to join the list - or just go to hell.
> > 
> > 
> > I've got a patch to do that against 2.0.8 which works for new lists.  If
> > someone could tell me what the right way is to add an entry into an existing
> > database I'd have a complete patch.
> 
> I think Mailman/versions.py::NewVars() is the one that does all that
> sort of stuff.  It's invoked when the version change is detected
> in MailList.py::CheckVersion(), though, and that version number
> is owned by Barry/the official distro.
> 
> I think it's probably difficult to do in a patch so that it doesn't
> interfere with future releases.
> 
> 
> _______________________________________________
> Mailman-Developers mailing list
> Mailman-Developers@python.org
> http://mail.python.org/mailman/listinfo/mailman-developers

-- 
The 5 year plan:
In five years we'll make up another plan.
Or just re-use this one.

--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="mailman-rejectpost.patch"

diff --new-file --exclude=~$ -r -u mailman-2.0.8.dist/Mailman/Defaults.py.in mailman-2.0.8/Mailman/Defaults.py.in
--- mailman-2.0.8.dist/Mailman/Defaults.py.in	Wed Nov 15 23:23:52 2000
+++ mailman-2.0.8/Mailman/Defaults.py.in	Tue Dec  4 03:15:34 2001
@@ -344,6 +344,8 @@
 # Make it 1 when it works.
 DEFAULT_MEMBER_POSTING_ONLY = 0
 
+# By default, admins must approve all posts.  Set to 1 to just bounce
+DEFAULT_AUTO_REJECT_NONMEMBERS = 0
 
 
 #####
diff --new-file --exclude=~$ -r -u mailman-2.0.8.dist/Mailman/Handlers/Hold.py mailman-2.0.8/Mailman/Handlers/Hold.py
--- mailman-2.0.8.dist/Mailman/Handlers/Hold.py	Thu May 31 17:05:44 2001
+++ mailman-2.0.8/Mailman/Handlers/Hold.py	Tue Dec  4 04:41:04 2001
@@ -149,8 +149,13 @@
            not Utils.FindMatchingAddresses(sender, posters):
             # the sender is neither a member of the list, nor in the list of
             # explicitly approved posters
-            hold_for_approval(mlist, msg, msgdata, NonMemberPost)
-            # no return
+            if mlist.auto_reject_nonmembers:
+                # The list has a strict members-only policy
+                reject_no_approval(mlist, msg, msgdata, NonMemberPost)
+            else:
+                # The list lets the admin make the call
+                hold_for_approval(mlist, msg, msgdata, NonMemberPost)
+                # no return
     elif mlist.posters:
         posters = Utils.List2Dict(map(string.lower, mlist.posters))
         if not Utils.FindMatchingAddresses(sender, posters):
@@ -237,6 +242,51 @@
     if not fromusenet and not mlist.dont_respond_to_post_requests:
         subject = 'Your message to %s awaits moderator approval' % listname
         text = Utils.maketext('postheld.txt', d)
+        msg = Message.UserNotification(sender, adminaddr, subject, text)
+        HandlerAPI.DeliverToUser(mlist, msg)
+    # Log the held message
+    syslog('vette', '%s post from %s held: %s' % (listname, sender, reason))
+    # raise the specific MessageHeld exception to exit out of the message
+    # delivery pipeline
+    raise exc
+
+
+
+# based on hold_for_approval() above.
+# at nylug, we want to bounce back a rejection message to users who
+# post without being members.  We don't want to have to spend time dealing
+# with the administrative overhead of checking and approving emails that we
+# clearly don't support as a matter of policy.
+# It sounds small, but its a good idea.  It should be an option in mailman.
+# -PN 20011128 <spacey@nylug.org>
+def reject_no_approval(mlist, msg, msgdata, exc):
+    if type(exc) is ClassType:
+        # Go ahead and instantiate it now.
+        exc = exc()
+    listname = mlist.real_name
+    reason = str(exc)
+    sender = msg.GetSender()
+    adminaddr = mlist.GetAdminEmail()
+    listurl = mlist.GetScriptURL('admin_url', absolute=1)
+    msgdata['rejection-notice'] = exc.rejection_notice(mlist)
+    d = {'listname'   : listname,
+         'hostname'   : mlist.host_name,
+         'reason'     : reason,
+         'sender'     : sender,
+         'subject'    : msg.get('subject', '(no subject)'),
+         'admindb_url': mlist.GetScriptURL('admindb', absolute=1),
+         'admindb_url': mlist.GetScriptURL('admindb', absolute=1),
+         'listurl'    : mlist.GetScriptURL('listinfo', absolute=1),
+         }
+    # mlist.HoldMessage(msg, reason, msgdata)
+    # now we need to craft and send a message to the list admin so they can
+    # deal with the held message
+    # Or not. -PN
+    # We may want to send a notification to the original sender too
+    fromusenet = msgdata.get('fromusenet')
+    if not fromusenet and not mlist.dont_respond_to_post_requests:
+        subject = 'Your message to %s has been rejected' % listname
+        text = Utils.maketext('postrejected.txt', d)
         msg = Message.UserNotification(sender, adminaddr, subject, text)
         HandlerAPI.DeliverToUser(mlist, msg)
     # Log the held message
diff --new-file --exclude=~$ -r -u mailman-2.0.8.dist/Mailman/Handlers/Hold.py~ mailman-2.0.8/Mailman/Handlers/Hold.py~
--- mailman-2.0.8.dist/Mailman/Handlers/Hold.py~	Wed Dec 31 19:00:00 1969
+++ mailman-2.0.8/Mailman/Handlers/Hold.py~	Tue Dec  4 02:33:58 2001
@@ -0,0 +1,296 @@
+# Copyright (C) 1998,1999,2000,2001 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
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software 
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+"""Determine whether this message should be held for approval.
+
+This modules tests only for hold situations, such as messages that are too
+large, messages that have potential administrivia, etc.  Definitive approvals
+or denials are handled by a different module.
+
+If no determination can be made (i.e. none of the hold criteria matches), then
+we do nothing.  If the message must be held for approval, then the hold
+database is updated and any administator notification messages are sent.
+Finally an exception is raised to let the pipeline machinery know that further
+message handling should stop.
+
+"""
+
+import os
+import string
+import time
+from types import ClassType
+
+try:
+    import cPickle
+    pickle = cPickle
+except ImportError:
+    import pickle
+
+import HandlerAPI
+from Mailman import Message
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman.Logging.Syslog import syslog
+
+
+
+class ForbiddenPoster(HandlerAPI.MessageHeld):
+    "Sender is explicitly forbidden"
+    rejection = 'You are forbidden from posting messages to this list.'
+
+class ModeratedPost(HandlerAPI.MessageHeld):
+    "Post to moderated list"
+    rejection = 'Your message has been deemed inappropriate by the moderator.'
+
+class NonMemberPost(HandlerAPI.MessageHeld):
+    "Post by non-member to a members-only list"
+    rejection = 'Non-members are not allowed to post messages to this list.'
+
+class NotExplicitlyAllowed(HandlerAPI.MessageHeld):
+    "Posting to a restricted list by sender requires approval"
+    rejection = 'This list is restricted; your message was not approved.'
+
+class TooManyRecipients(HandlerAPI.MessageHeld):
+    "Too many recipients to the message"
+    rejection = 'Please trim the recipient list; it is too long.'
+
+class ImplicitDestination(HandlerAPI.MessageHeld):
+    "Message has implicit destination"
+    rejection = '''Blind carbon copies or other implicit destinations are
+not allowed.  Try reposting your message by explicitly including the list
+address in the To: or Cc: fields.'''
+
+class Administrivia(HandlerAPI.MessageHeld):
+    "Message may contain administrivia"
+
+    def rejection_notice(self, mlist):
+        return """Please do *not* post administrative requests to the mailing
+list.  If you wish to subscribe, visit %(listurl)s or send a message with the
+word `help' in it to the request address, %(request)s, for further
+instructions.""" % {'listurl': mlist.GetScriptURL('listinfo', absolute=1),
+                    'request': mlist.GetRequestEmail(),
+                    }
+
+class SuspiciousHeaders(HandlerAPI.MessageHeld):
+    "Message has a suspicious header"
+    rejection = 'Your message had a suspicious header.'
+
+class MessageTooBig(HandlerAPI.MessageHeld):
+    "Message body is too big: %d bytes but there's a limit of %d KB"
+    def __init__(self, msgsize, limit):
+        self.__msgsize = msgsize
+        self.__limit = limit
+
+    def __str__(self):
+        return HandlerAPI.MessageHeld.__str__(self) % (
+            self.__msgsize, self.__limit)
+
+    def rejection_notice(self, mlist):
+        return """Your message was too big; please trim it to less than
+%(kb)s KB in size.""" % {'kb': mlist.max_message_size}
+
+
+
+def process(mlist, msg, msgdata):
+    if msgdata.get('approved'):
+        return
+    # get the sender of the message
+    listname = mlist.internal_name()
+    adminaddr = listname + '-admin'
+    sender = msg.GetSender()
+    # Special case an ugly sendmail feature: If there exists an alias of the
+    # form "owner-foo: bar" and sendmail receives mail for address "foo",
+    # sendmail will change the envelope sender of the message to "bar" before
+    # delivering.  This feature does not appear to be configurable.  *Boggle*.
+    if not sender or sender[:len(listname)+6] == adminaddr:
+        sender = msg.GetSender(use_envelope=0)
+    #
+    # possible administrivia?
+    if mlist.administrivia and Utils.IsAdministrivia(msg):
+        hold_for_approval(mlist, msg, msgdata, Administrivia)
+        # no return
+    #
+    # is the poster in the list of explicitly forbidden posters?
+    if len(mlist.forbidden_posters):
+        forbiddens = Utils.List2Dict(mlist.forbidden_posters)
+        addrs = Utils.FindMatchingAddresses(sender, forbiddens)
+        if addrs:
+            hold_for_approval(mlist, msg, msgdata, ForbiddenPoster)
+            # no return
+    #
+    # is the list moderated?  if so and the sender is not in the list of
+    # allowed posters then hold the message.
+    if mlist.moderated:
+        posters = Utils.List2Dict(mlist.posters)
+        addrs = Utils.FindMatchingAddresses(sender, posters)
+        if not addrs:
+            hold_for_approval(mlist, msg, msgdata, ModeratedPost)
+            # no return
+    #
+    # postings only from list members?  mlist.posters are allowed in addition
+    # to list members.  If not set, then only the members in posters are
+    # allowed to post without approval.
+    if mlist.member_posting_only:
+        posters = Utils.List2Dict(map(string.lower, mlist.posters))
+        if not mlist.IsMember(sender) and \
+           not Utils.FindMatchingAddresses(sender, posters):
+            # the sender is neither a member of the list, nor in the list of
+            # explicitly approved posters
+            if mlist.auto_reject_nonmembers:
+                # The list has a strict members-only policy
+                reject_no_approval(mlist, msg, msgdata, NonMemberPost)
+            else:
+                # The list lets the admin make the call
+                hold_for_approval(mlist, msg, msgdata, NonMemberPost)
+                # no return
+    elif mlist.posters:
+        posters = Utils.List2Dict(map(string.lower, mlist.posters))
+        if not Utils.FindMatchingAddresses(sender, posters):
+            # the sender is not explicitly in the list of allowed posters
+            # (which is non-empty), so hold the message
+            hold_for_approval(mlist, msg, msgdata, NotExplicitlyAllowed)
+            # no return
+    #
+    # are there too many recipients to the message?
+    if mlist.max_num_recipients > 0:
+        # figure out how many recipients there are
+        recips = []
+        toheader = msg.getheader('to')
+        if toheader:
+            recips = recips + map(string.strip, string.split(toheader, ','))
+        ccheader = msg.getheader('cc')
+        if ccheader:
+            recips = recips + map(string.strip, string.split(ccheader, ','))
+        if len(recips) > mlist.max_num_recipients:
+            hold_for_approval(mlist, msg, msgdata, TooManyRecipients)
+            # no return
+    #
+    # implicit destination?  Note that message originating from the Usenet
+    # side of the world should never be implicitly destined
+    if mlist.require_explicit_destination and \
+       not mlist.HasExplicitDest(msg) and \
+       not msgdata.get('fromusenet'):
+        # then
+        hold_for_approval(mlist, msg, msgdata, ImplicitDestination)
+        # no return
+    #
+    # suspicious headers?
+    if mlist.bounce_matching_headers:
+        triggered = mlist.HasMatchingHeader(msg)
+        if triggered:
+            # TBD: Darn - can't include the matching line for the admin
+            # message because the info would also go to the sender
+            hold_for_approval(mlist, msg, msgdata, SuspiciousHeaders)
+            # no return
+    #
+    # message too big?
+    if mlist.max_message_size > 0:
+        bodylen = len(msg.body)
+        if bodylen/1024.0 > mlist.max_message_size:
+            hold_for_approval(mlist, msg, msgdata,
+                              MessageTooBig(bodylen, mlist.max_message_size))
+            # no return
+
+
+
+def hold_for_approval(mlist, msg, msgdata, exc):
+    # TBD: This should really be tied into the email confirmation system so
+    # that the message can be approved or denied via email as well as the
+    # Web.  That's for later though, because it would mean a revamp of the
+    # MailCommandHandler too.
+    #
+    if type(exc) is ClassType:
+        # Go ahead and instantiate it now.
+        exc = exc()
+    listname = mlist.real_name
+    reason = str(exc)
+    sender = msg.GetSender()
+    adminaddr = mlist.GetAdminEmail()
+    msgdata['rejection-notice'] = exc.rejection_notice(mlist)
+    mlist.HoldMessage(msg, reason, msgdata)
+    # now we need to craft and send a message to the list admin so they can
+    # deal with the held message
+    d = {'listname'   : listname,
+         'hostname'   : mlist.host_name,
+         'reason'     : reason,
+         'sender'     : sender,
+         'subject'    : msg.get('subject', '(no subject)'),
+         'admindb_url': mlist.GetScriptURL('admindb', absolute=1),
+         }
+    if mlist.admin_immed_notify:
+        # get the text from the template
+        subject = '%s post from %s requires approval' % (listname, sender)
+        text = Utils.maketext('postauth.txt', d, raw=1)
+        # craft the admin notification message and deliver it
+        msg = Message.UserNotification(adminaddr, adminaddr, subject, text)
+        HandlerAPI.DeliverToUser(mlist, msg)
+    # We may want to send a notification to the original sender too
+    fromusenet = msgdata.get('fromusenet')
+    if not fromusenet and not mlist.dont_respond_to_post_requests:
+        subject = 'Your message to %s awaits moderator approval' % listname
+        text = Utils.maketext('postheld.txt', d)
+        msg = Message.UserNotification(sender, adminaddr, subject, text)
+        HandlerAPI.DeliverToUser(mlist, msg)
+    # Log the held message
+    syslog('vette', '%s post from %s held: %s' % (listname, sender, reason))
+    # raise the specific MessageHeld exception to exit out of the message
+    # delivery pipeline
+    raise exc
+
+
+
+# based on hold_for_approval() above.
+# at nylug, we want to bounce back a rejection message to users who
+# post without being members.  We don't want to have to spend time dealing
+# with the administrative overhead of checking and approving emails that we
+# clearly don't support as a matter of policy.
+# It sounds small, but its a good idea.  It should be an option in mailman.
+# -PN 20011128 <spacey@nylug.org>
+def reject_no_approval(mlist, msg, msgdata, exc):
+    if type(exc) is ClassType:
+        # Go ahead and instantiate it now.
+        exc = exc()
+    listname = mlist.real_name
+    reason = str(exc)
+    sender = msg.GetSender()
+    adminaddr = mlist.GetAdminEmail()
+    listurl = mlist.GetScriptURL('admin_url', absolute=1)
+    msgdata['rejection-notice'] = exc.rejection_notice(mlist)
+    d = {'listname'   : listname,
+         'hostname'   : mlist.host_name,
+         'reason'     : reason,
+         'sender'     : sender,
+         'subject'    : msg.get('subject', '(no subject)'),
+         'admindb_url': mlist.GetScriptURL('admindb', absolute=1),
+         'admindb_url': mlist.GetScriptURL('admindb', absolute=1),
+         'listurl'    : mlist.GetScriptURL('listinfo', absolute=1),
+         }
+    # mlist.HoldMessage(msg, reason, msgdata)
+    # now we need to craft and send a message to the list admin so they can
+    # deal with the held message
+    # Or not. -PN
+    # We may want to send a notification to the original sender too
+    fromusenet = msgdata.get('fromusenet')
+    if not fromusenet and not mlist.dont_respond_to_post_requests:
+        subject = 'Your message to %s has been rejected' % listname
+        text = Utils.maketext('postrejected.txt', d)
+        msg = Message.UserNotification(sender, adminaddr, subject, text)
+        HandlerAPI.DeliverToUser(mlist, msg)
+    # Log the held message
+     syslog('vette', '%s post from %s held: %s' % (listname, sender, reason))
+     # raise the specific MessageHeld exception to exit out of the message
+     # delivery pipeline
+     raise exc
diff --new-file --exclude=~$ -r -u mailman-2.0.8.dist/Mailman/MailList.py mailman-2.0.8/Mailman/MailList.py
--- mailman-2.0.8.dist/Mailman/MailList.py	Tue May 29 10:45:27 2001
+++ mailman-2.0.8/Mailman/MailList.py	Tue Dec  4 04:00:57 2001
@@ -323,6 +323,7 @@
 	self.private_roster = mm_cfg.DEFAULT_PRIVATE_ROSTER
 	self.obscure_addresses = mm_cfg.DEFAULT_OBSCURE_ADDRESSES
 	self.member_posting_only = mm_cfg.DEFAULT_MEMBER_POSTING_ONLY
+        self.auto_reject_nonmembers = mm_cfg.DEFAULT_AUTO_REJECT_NONMEMBERS
 	self.host_name = mm_cfg.DEFAULT_HOST_NAME
         self.admin_member_chunksize = mm_cfg.DEFAULT_ADMIN_MEMBER_CHUNKSIZE
         self.administrivia = mm_cfg.DEFAULT_ADMINISTRIVIA
@@ -660,6 +661,16 @@
              " If you want list members to be able to"
              " post, plus a handful of other posters, see the <i> posters </i>"
              " setting below"),
+
+            ('auto_reject_nonmembers', mm_cfg.Radio, ('No', 'Yes'), 0,
+             'Automate rejection of posts from non-subscribers?'
+             ' (<i>auto_reject_nonmembers</i>)',
+
+             "Use this option if you have enabled member_posting_only and"
+             " you're sure that this list will only ever want posts from"
+             " subscribed addresses.  This will automaticly send back a"
+             " message to the sender asking them to subscribe to the list"
+             " or to send from their subscribed address."),
 
 	    ('posters', mm_cfg.EmailList, (5, WIDTH), 1,
              'Addresses of members accepted for posting to this'
diff --new-file --exclude=~$ -r -u mailman-2.0.8.dist/Mailman/versions.py mailman-2.0.8/Mailman/versions.py
--- mailman-2.0.8.dist/Mailman/versions.py	Tue Jul 10 10:58:56 2001
+++ mailman-2.0.8/Mailman/versions.py	Tue Dec  4 03:05:31 2001
@@ -82,6 +82,10 @@
         if not hasattr(l, newname) and newdefault is not uniqueval:
                 setattr(l, newname, newdefault)
 
+    # Add the option to not have to admin bouncing messages
+    if not hasattr(l, "auto_reject_nonmembers"):
+        setattr(l, "auto_reject_nonmembers", mm_cfg.DEFAULT_AUTO_REJECT_NONMEMBERS)
+
     # Migrate to 1.0b6, klm 10/22/1998:
     PreferStored('reminders_to_admins', 'umbrella_list',
                  mm_cfg.DEFAULT_UMBRELLA_LIST)
diff --new-file --exclude=~$ -r -u mailman-2.0.8.dist/templates/postrejected.txt mailman-2.0.8/templates/postrejected.txt
--- mailman-2.0.8.dist/templates/postrejected.txt	Wed Dec 31 19:00:00 1969
+++ mailman-2.0.8/templates/postrejected.txt	Tue Dec  4 02:33:58 2001
@@ -0,0 +1,17 @@
+Your mail to '%(listname)s' with the subject
+          
+    %(subject)s
+    
+Has been rejected because
+    
+    %(reason)s
+    
+If you would like to post messages to this list, please
+make sure you are subscribed and sending from your 
+subscribed address.  
+
+Visit 
+
+    %(listurl)s 
+
+If you are not yet a subscriber.

--Q68bSM7Ycu6FN28Q--