[Mailman-developers] confirmation of subscriptions

Scott scott@chronis.icgroup.com
Wed, 15 Apr 1998 06:31:42 -0400


i have developed a working confirmation step in the subscription
process for mailman. 

there are some things that should be presented for discussion and or
polished up before this addition (hopefully) makes it to the
distribution. 

below i've attached a patch to be applied to files in the
mailman/modules directory, and an extra module (mm_pending.py). 
in addtion, i'll put a distribution of the changed modules/*.py files 
at ftp://chronis.icgroup.com/pub/mmconfirm.tgz

all comments and feedback on the following are most welcome.


CHANGE ROSTER:

new mm_defaults variable DEFAULT_CONFIRM_SUBSCRIBE

the admin/listname/privacy page no longer presents the
web_subscribe_requires_confirmation, instead it present a new
confirm_subscribe option.

the subscribe command now uses the following syntax (backwards
compatible): 

subscribe [arg [arg [arg]]]

where <arg> can be one of: password, digest|nodigest or
address=<subscribe address>.

anytime the address= is specified, the confirmation step is triggered
regardless of list preference.  As an administrator of a high volume
mailing site, i strongly encourage us to keep it this way.  this will
also allow users to subscribe with forwarding addresses.

there is a new mail command:
confirm <confirmation number>

if the confirmation number represents an outstanding subscribe request
that is pending confirmation, then the subscription will take place.


TODO:

make email interface to unsubscribe address=

get totally rid of web_subscribe_requires_confirmation in the code
because it is no longer visible

check to make sure the address= address is being checked as a valid
email address.

clean up mm_pending.py with __version__ and stuff.


NOTE:
i'm getting buggy output of the mm_deliver.SUBSCRIBEACKTEXT, and am
not sure if it is the result of any of my changes, though it looks
like a bug unrelated to these changes at a glance.

Scott
-----------------------------------------------------------------------------
				PATCH
-----------------------------------------------------------------------------
diff -c -r /tmp/mailman/modules/maillist.py ./maillist.py
*** /tmp/mailman/modules/maillist.py	Sun Apr 12 00:34:01 1998
--- ./maillist.py	Wed Apr 15 05:19:56 1998
***************
*** 139,144 ****
--- 139,145 ----
  	self.welcome_msg = ''
  	self.goodbye_msg = ''
  	self.open_subscribe = mm_cfg.DEFAULT_OPEN_SUBSCRIBE
+         self.confirm_subscribe = mm_cfg.DEFAULT_CONFIRM_SUBSCRIBE
  	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
***************
*** 282,297 ****
               " members are admitted only at the discretion of the list"
  	     " administrator."),
  
! 	    ('web_subscribe_requires_confirmation', mm_cfg.Radio,
! 	     ('None', 'Requestor confirms via email', 'Admin approves'), 0,
! 	     'What confirmation is required for on-the-web subscribes?',
! 
!              "This option determines whether web-initiated subscribes"
!              " require further confirmation, either from the subscribed"
!              " address or from the list administrator.  Absence of"
!              " <em>any</em> confirmation makes web-based subscription a"
!              " tempting opportunity for mischievous subscriptions by third"
!              " parties."),
  
              "Membership exposure",
  
--- 283,296 ----
               " members are admitted only at the discretion of the list"
  	     " administrator."),
  
!              ('confirm_subscribe', mm_cfg.Radio, ('No', 'Yes'), 1,
!               'Should subscriptions require confirmation step?',
! 
!               "This option forces all subscription requests to prompt a verification"
!               " message to be sent to the subscribing address, and a response from that"
!               " address before subscription takes place.  This prevents malicious"
!               " folk from (mass) subscribing unconsenting owners of email addresses to"
!               " mailing lists."),
  
              "Membership exposure",
diff -c -r /tmp/mailman/modules/mm_defaults.py ./mm_defaults.py
*** /tmp/mailman/modules/mm_defaults.py	Mon Apr 13 14:09:27 1998
--- ./mm_defaults.py	Wed Apr 15 05:30:58 1998
***************
*** 81,86 ****
--- 81,88 ----
  DEFAULT_REPLY_GOES_TO_LIST = 0
  # Admin approval unnecessary for subscribes?
  DEFAULT_OPEN_SUBSCRIBE = 1
+ # confirm neccesary for subscribes?
+ DEFAULT_CONFIRM_SUBSCRIBE = 1
  # Private_roster == 0: anyone can see, 1: members only, 2: admin only.
  DEFAULT_PRIVATE_ROSTER = 0
  # When exposing members, make them unrecognizable as email addrs.  To
***************
*** 88,96 ****
  DEFAULT_OBSCURE_ADDRESSES = 1
  # Make it 1 when it works.
  DEFAULT_MEMBER_POSTING_ONLY = 0
- # 1 for email subscription verification, 2 for admin confirmation:
- DEFAULT_WEB_SUBSCRIBE_REQUIRES_CONFIRMATION = 1
- 
  		     # Digestification Defaults #
  
  # Will list be available in non-digested form?
--- 90,95 ----
diff -c -r /tmp/mailman/modules/mm_mailcmd.py ./mm_mailcmd.py
*** /tmp/mailman/modules/mm_mailcmd.py	Thu Apr  9 19:48:30 1998
--- ./mm_mailcmd.py	Wed Apr 15 05:28:59 1998
***************
*** 6,12 ****
  # Not implemented: get / index / which.
  
  import string, os, sys
! import mm_message, mm_err, mm_cfg, mm_utils
  
  option_descs = { 'digest' :
  		     'receive mail from the list bundled together instead of '
--- 6,12 ----
  # Not implemented: get / index / which.
  
  import string, os, sys
! import mm_message, mm_err, mm_cfg, mm_utils, mm_pending
  
  option_descs = { 'digest' :
  		     'receive mail from the list bundled together instead of '
***************
*** 47,53 ****
--- 47,55 ----
  	    'set' : self.ProcessSetCmd,
  	    'options' : self.ProcessOptionsCmd,
  	    'password' : self.ProcessPasswordCmd,
+             'confirm': self.ProcessConfirmCmd
  	    }
+         self.__noMailCmdResponse = 0
  
      def AddToResponse(self, text):
  	self._response_buffer = self._response_buffer + text + "\n"
***************
*** 84,90 ****
  		self.AddError("%s: Command UNKNOWN." % cmd)
  	    else:
  		self._cmd_dispatch[cmd](args, line, mail)
! 	self.SendMailCmdResponse(mail)
  
      def SendMailCmdResponse(self, mail):
  	self.SendTextToUser(subject = 'Mailman results for %s' % 
--- 86,93 ----
  		self.AddError("%s: Command UNKNOWN." % cmd)
  	    else:
  		self._cmd_dispatch[cmd](args, line, mail)
!         if not self.__noMailCmdResponse:
!             self.SendMailCmdResponse(mail)
  
      def SendMailCmdResponse(self, mail):
  	self.SendTextToUser(subject = 'Mailman results for %s' % 
***************
*** 338,386 ****
  	    self.AddError("%s %s" % (sys.exc_type, sys.exc_value))
  	    self.AddError("%s" % sys.exc_traceback)
  
      def ProcessSubscribeCmd(self, args, cmd, mail):
  	digest = self.digest_is_default
  	if not len(args):
  	    password = "%s%s" % (mm_utils.GetRandomSeed(), 
  				 mm_utils.GetRandomSeed())
! 	elif len(args) == 1:
! 	    if string.lower(args[0]) == 'digest':
! 		digest = 1
! 		password = "%s%s" % (mm_utils.GetRandomSeed(), 
! 				 mm_utils.GetRandomSeed())
! 	    elif string.lower(args[0]) == 'nodigest':
! 		digest = 0
! 		password = "%s%s" % (mm_utils.GetRandomSeed(), 
  				 mm_utils.GetRandomSeed())
! 	    else:
! 		password = args[0]
! 
! 	elif len(args) == 2:
! 	    if string.lower(args[1]) == 'nodigest':
! 		digest = 0
! 		password = args[0]
! 	    elif string.lower(args[1]) == 'digest':
! 		digest = 1
! 		password = args[0]
! 	    elif string.lower(args[0]) == 'nodigest':
! 		digest = 0
! 		password = args[1]
! 	    elif string.lower(args[0]) == 'digest':
! 		digest = 1
! 		password = args[1]
! 	    else:
! 		self.AddError("Usage: subscribe [password] [digest|nodigest]")
! 		return
! 	elif len(args) > 2:
! 		self.AddError("Usage: subscribe [password] [digest|nodigest]")
! 		return
  
  	try:
! 	    self.AddMember(mail.GetSender(), password, digest)
  	    self.AddToResponse("Succeeded.")
  	except mm_err.MMBadEmailError:
  	    self.AddError("Email address '%s' not accepted by Mailman." % 
! 			  mail.GetSender())
  	except mm_err.MMMustDigestError:
  	    self.AddError("List only accepts digest members.")
  	except mm_err.MMCantDigestError:
--- 341,410 ----
  	    self.AddError("%s %s" % (sys.exc_type, sys.exc_value))
  	    self.AddError("%s" % sys.exc_traceback)
  
+ 
+ 
+ 
      def ProcessSubscribeCmd(self, args, cmd, mail):
  	digest = self.digest_is_default
+         password = ""
+         address = ""
+         done_digest = 0
  	if not len(args):
  	    password = "%s%s" % (mm_utils.GetRandomSeed(), 
  				 mm_utils.GetRandomSeed())
!         elif len(args) > 3:
!             self.AddError("Usage: subscribe [password] [digest|nodigest]")
!             return
!         else:
!             for arg in args:
!                 if string.lower(arg) == 'digest' and not done_digest:
!                     digest = 1
!                     done_digest = 1
!                 elif string.lower(arg) == 'nodigest' and not done_digest:
!                     digest = 0
!                     done_digest = 1
!                 elif string.lower(arg)[:8] == 'address=' and not address:
!                     address = string.lower(arg)[8:]
!                 elif not password:
!                     password = arg
!                 else:
!                     self.AddError("Usage: subscribe [arg [arg [arg]]]  where <arg> can be\n"
!                                   "\t[password]\n\t[digest|nodigest]\nor"
!                                   "\t[address=<email address>]")
!                     return
!         if not password:
!             password = "%s%s" % (mm_utils.GetRandomSeed(), 
  				 mm_utils.GetRandomSeed())
!         # we don't want people subscribing addresses other than the sender address
!         # without an extra confirmation step
!         if address or self.confirm_subscribe: 
!             if not address:
!                 pending_addr = mail.GetSender()
!             else:
!                 pending_addr = address
!             
!             cookie = mm_pending.gencookie()
!             mm_pending.add2pending(pending_addr, password, digest, cookie)
!             self.SendTextToUser(subject = "%s -- confirmation of subscription req. %d" % \
!                                 (self.real_name, cookie),
!                                 recipient = pending_addr,
!                                 sender = self.GetRequestEmail(),
!                                 text = mm_pending.VERIFY_FMT % ({"email": pending_addr,
!                                                                  "listaddress": self.GetListEmail(),
!                                                                  "listname": self.real_name,
!                                                                  "cookie": cookie,
!                                                                  "requestor": mail.GetSender()}))
!             self.__noMailCmdResponse = 1
!             return
!         self.FinishSubscribe(mail.GetSender(), password, digest)
  
+     def FinishSubscribe(self, addr, password, digest):
  	try:
! 	    self.AddMember(addr, password, digest)
  	    self.AddToResponse("Succeeded.")
  	except mm_err.MMBadEmailError:
  	    self.AddError("Email address '%s' not accepted by Mailman." % 
! 			  addr)
  	except mm_err.MMMustDigestError:
  	    self.AddError("List only accepts digest members.")
  	except mm_err.MMCantDigestError:
***************
*** 391,406 ****
  	    self.AddApprovalMsg(cmd)
          except mm_err.MMHostileAddress:
  	    self.AddError("Email address '%s' not accepted by Mailman "
! 			  "(insecure address)" % mail.GetSender())
  	except mm_err.MMAlreadyAMember:
! 	    self.AddError("%s is already a list member." % mail.GetSender())
  	except:
  	    # TODO: Should log the error we got if we got here.
  	    self.AddError("An unknown Mailman error occured.")
  	    self.AddError("Please forward on your request to %s" %
  			  self.GetAdminEmail())
  	    self.AddError("%s" % sys.exc_type)
! 		
      def AddApprovalMsg(self, cmd):
          self.AddError('''Your request to %s:
  
--- 415,451 ----
  	    self.AddApprovalMsg(cmd)
          except mm_err.MMHostileAddress:
  	    self.AddError("Email address '%s' not accepted by Mailman "
! 			  "(insecure address)" % addr)
  	except mm_err.MMAlreadyAMember:
! 	    self.AddError("%s is already a list member." % addr)
  	except:
  	    # TODO: Should log the error we got if we got here.
  	    self.AddError("An unknown Mailman error occured.")
  	    self.AddError("Please forward on your request to %s" %
  			  self.GetAdminEmail())
  	    self.AddError("%s" % sys.exc_type)
!         
! 
!     def ProcessConfirmCmd(self, args, cmd, mail):
!         if len(args) != 1:
!             self.AddError("Usage: confirm <confirmation number>\n")
!             return
!         try:
!             cookie = string.atoi(args[0])
!         except:
!             self.AddError("Usage: confirm <confirmation number>\n")
!             return
!         pending = mm_pending.get_pending()
!         if not pending.has_key(cookie):
!             self.AddError("Invalid confirmation number!\n"
!                           "Please recheck the confirmation number and try again.")
!             return
!         (email_addr, password, digest, ts) = pending[cookie]
!         self.FinishSubscribe(email_addr, password, digest)
!         del pending[cookie]
!         mm_pending.set_pending(pending)
!         
!     
      def AddApprovalMsg(self, cmd):
          self.AddError('''Your request to %s:





----------------------------------------------------------------------
			    mm_pending.py
----------------------------------------------------------------------
"""
module for handling pending subscriptions
"""

import os 
import sys
import posixfile
import marshal
import time
import rand
import mm_cfg

DB_PATH = mm_cfg.MAILMAN_DIR + "/misc/pending_subscriptions.db"
LOCK_PATH = mm_cfg.LOCK_DIR + "/pending_subscriptions.lock"


VERIFY_FMT = """\
You or someone (%(requestor)s) has requested that your email
address (%(email)s) be subscribed to the %(listname)s mailling
list at %(listaddress)s.  If you wish to fulfill this request,
please reply to this message, with the following line, and only
the following line in the message body:

confirm %(cookie)s

If you do not wish to subscribe to this list, please simply ignore  
or delete this message.
"""

# ' icky emacs font lock thing


def get_pending():
    " returns a dict containing pending information"
    try:
        fp = open(DB_PATH,"r" )
    except IOError:
        return {}
    dict = marshal.load(fp)
    return dict


def gencookie(p=None):
    if p is None:
        p = get_pending()
    while 1:
        newcookie = rand.rand()
        if p.has_key(newcookie):
            continue
        return newcookie

def set_pending(p):
    ou = os.umask(0)  
    try: 
        lock_file = posixfile.open(LOCK_PATH,'a+') 
    finally: 
        os.umask(ou) 
    lock_file.lock('w|', 1) 
    fp = open(DB_PATH, "w") 
    marshal.dump(p, fp) 
    fp.close() 
    lock_file.lock("u") 
    lock_file.close() 


def add2pending(email_addr, password, digest, cookie): 
    ts = int(time.time())
    processed = 0
    p = get_pending()
    p[cookie] = (email_addr, password, digest,  ts)
    set_pending(p)


def set_processed(cookie, value):
    p = get_pending()
    if p.has_key(cookie):
        (email_addr, listname, password, digest, processed, ts) = p[cookie]
        processed = value
        p[cookie] = (email_addr, listname, password, digest, processed, ts)
        set_pending(p)
    else:
        raise ValueError, "attempt to set processed field in pending to non existent cookie (%d)" % (cookie)