[Mailman-Developers] unsubscriptions requiring approval

Thomas Wouters thomas@xs4all.net
Tue, 8 Feb 2000 02:05:27 +0100


--ZPt4rx8FFjLCG7dd
Content-Type: text/plain; charset=us-ascii


Another minor patch adding what I need to mailman ;) I also noticed this on
the jitterbug list, so I guess more people had wished or at least wondered
for it ;P I'm posting it mostly to see if it generates comments, and for me
to find a good excuse to sleep. It's functional, it works on my test-list,
but some of it (most notably the english -- is there a good word for
'unsubscription request' ?) sucks.

Adding this has been mostly an exercise to learn a bit about the Mailman
insides, and to see how easily it can be done; fairly easily ;) Comments are
welcome, including "d'oh, dont write that, i already wrote it !"

The patchs misses the file 'templates/unsubauth.txt' by the way.. Just copy
subauth.txt and change 'subscr' into 'unsubscr' ;P

Sleepy-ly y'rs,
-- 
Thomas Wouters <thomas@xs4all.net>

Hi! I'm a .signature virus! copy me into your .signature file to help me spread!

--ZPt4rx8FFjLCG7dd
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="mailman-unsub-approval.diff"

? templates/unsubauth.txt
Index: Mailman/Bouncer.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/Bouncer.py,v
retrieving revision 1.37
diff -u -r1.37 Bouncer.py
--- Bouncer.py	1999/12/16 17:11:04	1.37
+++ Bouncer.py	2000/02/08 00:57:47
@@ -299,7 +299,7 @@
                         self.real_name, addr, reason)
             return reason, 1
 	try:
-	    self.DeleteMember(addr, "bouncing addr")
+	    self.ApprovedDeleteMember(addr, "bouncing addr")
 	    self.LogMsg("bounce", "%s: removed %s", self.real_name, addr) 
             self.Save()
             return 1, 1
Index: Mailman/Defaults.py.in
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/Defaults.py.in,v
retrieving revision 1.90
diff -u -r1.90 Defaults.py.in
--- Defaults.py.in	1999/11/30 23:10:57	1.90
+++ Defaults.py.in	2000/02/08 00:57:47
@@ -344,4 +344,4 @@
 from Version import VERSION
 
 # Data file version number
-DATA_FILE_VERSION = 15
+DATA_FILE_VERSION = 16
Index: Mailman/HTMLFormatter.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/HTMLFormatter.py,v
retrieving revision 1.46
diff -u -r1.46 HTMLFormatter.py
--- HTMLFormatter.py	1999/11/24 21:15:21	1.46
+++ HTMLFormatter.py	2000/02/08 00:57:47
@@ -206,6 +206,11 @@
                          " request will be sent to the '%s' account for"
                          " your address.)" % self.umbrella_member_suffix)
 
+        if self.unsubscribe_policy == 1:
+            msg = msg + ("<p>(Please note that this list does not allow"
+                         " members to unsubscribe themselves without"
+                         " administrator approval. Unsubscription requests"
+                         " will get forwarded to the mailinglist administrator.)")
         return msg
 
     def FormatUndigestButton(self):
Index: Mailman/ListAdmin.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/ListAdmin.py,v
retrieving revision 1.27
diff -u -r1.27 ListAdmin.py
--- ListAdmin.py	1999/11/24 21:13:59	1.27
+++ ListAdmin.py	2000/02/08 00:57:48
@@ -40,6 +40,7 @@
 # Request types requiring admin approval
 HELDMSG = 1
 SUBSCRIPTION = 2
+UNSUBSCRIPTION = 3
 
 
 
@@ -106,6 +107,9 @@
     def GetSubscriptionIds(self):
         return self.__getmsgids(SUBSCRIPTION)
 
+    def GetUnsubscriptionIds(self):
+        return self.__getmsgids(UNSUBSCRIPTION)
+
     def GetRecord(self, id):
         self.__opendb()
         type, data = self.__db[id]
@@ -122,9 +126,11 @@
         del self.__db[id]
         if rtype == HELDMSG:
             self.__handlepost(data, value, comment)
-        else:
-            assert rtype == SUBSCRIPTION
+        elif rtype == SUBSCRIPTION:
             self.__handlesubscription(data, value, comment)
+        else:
+            assert rtype == UNSUBSCRIPTION
+            self.__handleunsubscription(data, value, comment)
 
     def HoldMessage(self, msg, reason):
         # assure that the database is open for writing
@@ -258,6 +264,53 @@
             assert value == 1
             self.ApprovedAddMember(addr, password, digest)
 
+    def HoldUnsubscription(self, addr, whence, admin_notif):
+        # assure that the database is open for writing
+        self.__opendb()
+        # get the next unique id
+        id = self.__request_id()
+        assert not self.__db.has_key(id)
+        #
+        # save the information to the request database. for held subscription
+        # entries, each record in the database will be one of the following
+        # format:
+        #
+        # the time the subscription request was received
+        # the subscriber's address
+        # the log message
+        # wether or not to send an unsubscription notify to the list admin
+        #
+        data = time.time(), addr, whence, admin_notif
+        self.__db[id] = (UNSUBSCRIPTION, data)
+        #
+        # TBD: this really shouldn't go here but I'm not sure where else is
+        # appropriate.
+        self.LogMsg('vette', '%s: held unsubscription request from %s' %
+                    (self.real_name, addr))
+        # possibly notify the administrator
+        if self.admin_immed_notify:
+            subject = 'New unsubscription request to list %s from %s' % (
+                self.real_name, addr)
+            text = Utils.maketext(
+                'unsubauth.txt',
+                {'username'   : addr,
+                 'listname'   : self.real_name,
+                 'hostname'   : self.host_name,
+                 'admindb_url': self.GetAbsoluteScriptURL('admindb'),
+                 })
+            adminaddr = self.GetAdminEmail()
+            msg = Message.UserNotification(adminaddr, adminaddr, subject, text)
+            HandlerAPI.DeliverToUser(self, msg)
+
+    def __handleunsubscription(self, record, value, comment):
+        stime, addr, whence, admin_notif = record
+        if value == 0:
+            # refused
+            self.__refuse('Unsubscription request', addr, comment)
+        else:
+            # subscribe
+            assert value == 1
+            self.ApprovedDeleteMember(addr, whence, admin_notif)
 
     def __refuse(self, request, recip, comment, origmsg=None):
         adminaddr = self.GetAdminEmail()
Index: Mailman/MailCommandHandler.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/MailCommandHandler.py,v
retrieving revision 1.59
diff -u -r1.59 MailCommandHandler.py
--- MailCommandHandler.py	1999/12/16 17:13:28	1.59
+++ MailCommandHandler.py	2000/02/08 00:57:48
@@ -500,6 +500,8 @@
 	    self.ConfirmUserPassword(addr, args[0])
 	    self.DeleteMember(addr, "mailcmd")
 	    self.AddToResponse("Succeeded.")
+        except Errors.MMNeedApproval:
+            self.AddToResponse("Unsubscribing needs admininistrator approval.\nYour request has been forwarded to the list administrator.")
 	except Errors.MMListNotReady:
 	    self.AddError("List is not functional.")
 	except (Errors.MMNoSuchUserError, Errors.MMNotAMemberError):
Index: Mailman/MailList.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/MailList.py,v
retrieving revision 1.151
diff -u -r1.151 MailList.py
--- MailList.py	1999/12/27 22:54:55	1.151
+++ MailList.py	2000/02/08 00:57:49
@@ -339,6 +339,7 @@
 	self.welcome_msg = ''
 	self.goodbye_msg = ''
 	self.subscribe_policy = mm_cfg.DEFAULT_SUBSCRIBE_POLICY
+	self.unsubscribe_policy = mm_cfg.DEFAULT_UNSUBSCRIBE_POLICY
 	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
@@ -599,6 +600,17 @@
 
             sub_cfentry,
             
+            ('unsubscribe_policy', mm_cfg.Radio, 
+             ('none', 'require approval'), 0,
+             "What steps are required for unsubscription?<br>",
+             "none - anyone can unsubscribe using email or the webpage<br>"
+             "require approval - require list administrator approval"
+             " for unsubscriptions<br>"
+             "<p>Please leave the unsubscription 'open' unless you have a"
+             " very good reason to require approval. Leaving people on lists"
+             " against their will can easily be seen as spamming or harassment"
+            ),
+            
             "Membership exposure",
 
 	    ('private_roster', mm_cfg.Radio,
@@ -1083,7 +1095,7 @@
                         HandlerAPI.DeliverToUser(self, msg)
         return result
 
-    def DeleteMember(self, name, whence=None, admin_notif=None):
+    def ApprovedDeleteMember(self, name, whence=None, admin_notif=None):
 	self.IsListInitialized()
         # FindMatchingAddresses *should* never return more than 1 address.
         # However, should log this, just to make sure.
@@ -1139,6 +1151,15 @@
             whence = ""
         self.LogMsg("subscribe", "%s: deleted %s%s",
                     self._internal_name, name, whence)
+
+    def DeleteMember(self, name, whence=None, admin_notif=None):
+	self.IsListInitialized()
+        # FindMatchingAddresses *should* never return more than 1 address.
+        # However, should log this, just to make sure.
+        if self.unsubscribe_policy == 1:
+            self.HoldUnsubscription(name, whence, admin_notif)
+            raise Errors.MMNeedApproval
+        ApprovedDeleteMember(name, whence, admin_notif)
 
     def IsMember(self, address):
 	return len(Utils.FindMatchingAddresses(address, self.members,
Index: Mailman/Cgi/admin.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/Cgi/admin.py,v
retrieving revision 1.53
diff -u -r1.53 admin.py
--- admin.py	1999/12/27 22:28:07	1.53
+++ admin.py	2000/02/08 00:57:50
@@ -887,7 +887,7 @@
         for user in users:
             if not cgi_info.has_key('%s_subscribed' % (user)):
                 try:
-                    mlist.DeleteMember(user)
+                    mlist.ApprovedDeleteMember(user)
                     dirty = 1
                 except Errors.MMNoSuchUserError:
                     unsubscribe_errors.append((user, 'Not subscribed'))
Index: Mailman/Cgi/admindb.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/Cgi/admindb.py,v
retrieving revision 1.19
diff -u -r1.19 admindb.py
--- admindb.py	1999/11/15 22:29:46	1.19
+++ admindb.py	2000/02/08 00:57:50
@@ -164,6 +164,19 @@
         for id in subpendings:
 	    PrintAddMemberRequest(mlist, id, t)
 	form.AddItem(t)
+    unsubpendings = mlist.GetUnsubscriptionIds()
+    if unsubpendings:
+        form.AddItem('<hr>')
+	form.AddItem(Center(Header(2, 'Unsubscription Requests')))
+	t = Table(border=2)
+	t.AddRow([
+	    Bold('Address'),
+	    Bold('Your Decision'),
+	    Bold('Reason for unsubscription refusal (optional)')
+            ])
+        for id in unsubpendings:
+	    PrintDeleteMemberRequest(mlist, id, t)
+	form.AddItem(t)
     # Post holds are now handled differently
     heldmsgs = mlist.GetHeldMessageIds()
     total = len(heldmsgs)
@@ -183,6 +196,13 @@
     time, addr, passwd, digest = mlist.GetRecord(id)
     table.AddRow([addr,
                   RadioButtonArray(id, ('Refuse', 'Subscribe')),
+                  TextBox('comment-%d' % id, size=30)
+                  ])
+
+def PrintDeleteMemberRequest(mlist, id, table):
+    time, addr, whence, admin_notif = mlist.GetRecord(id)
+    table.AddRow([addr,
+                  RadioButtonArray(id, ('Refuse', 'Unsubscribe')),
                   TextBox('comment-%d' % id, size=30)
                   ])
 
Index: Mailman/Cgi/handle_opts.py
===================================================================
RCS file: /projects/cvsroot/mailman/Mailman/Cgi/handle_opts.py,v
retrieving revision 1.18
diff -u -r1.18 handle_opts.py
--- handle_opts.py	2000/01/21 20:30:51	1.18
+++ handle_opts.py	2000/02/08 00:57:50
@@ -101,6 +101,12 @@
                 pw = form["upw"].value
                 if mlist.ConfirmUserPassword(user, pw):
                     mlist.DeleteMember(user, "web cmd")
+            except Errors.MMNeedApproval:
+                PrintResults(mlist, operation, doc,
+                             "Unsubscribing from this list requires "
+                             "administrator approval. Your request has been "
+                             "forwarded. Please contact the list administrator "
+                             "if you have any questions.")
             except Errors.MMListNotReady:
                 PrintResults(mlist, operation, doc, "List is not functional.")
             except Errors.MMNoSuchUserError:
Index: bin/remove_members
===================================================================
RCS file: /projects/cvsroot/mailman/bin/remove_members,v
retrieving revision 1.3
diff -u -r1.3 remove_members
--- remove_members	1999/03/04 17:18:24	1.3
+++ remove_members	2000/02/08 00:57:50
@@ -100,7 +100,7 @@
 
     for addr in addresses:
         try:
-            mlist.DeleteMember(addr)
+            mlist.ApprovedDeleteMember(addr)
         except Errors.MMNoSuchUserError:
             print "User `%s' not found." % addr
 
Index: bin/sync_members
===================================================================
RCS file: /projects/cvsroot/mailman/bin/sync_members,v
retrieving revision 1.8
diff -u -r1.8 sync_members
--- sync_members	1999/11/30 21:17:33	1.8
+++ sync_members	2000/02/08 00:57:50
@@ -244,7 +244,7 @@
         for laddr, addr in addrs.items():
             # should be a member, otherwise our test above is broken
             if not dryrun:
-                mlist.DeleteMember(addr, admin_notif=notifyadmin)
+                mlist.ApprovedDeleteMember(addr, admin_notif=notifyadmin)
             print 'Removed: %30s (%30s)' % (laddr, addr)
 
         mlist.Save()

--ZPt4rx8FFjLCG7dd--