confirmation of subscriptions

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.private_roster = mm_cfg.DEFAULT_PRIVATE_ROSTER self.obscure_addresses = mm_cfg.DEFAULT_OBSCURE_ADDRESSES self.member_posting_only = mm_cfg.DEFAULT_MEMBER_POSTING_ONLYself.confirm_subscribe = mm_cfg.DEFAULT_CONFIRM_SUBSCRIBE
*** 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
# 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# Digestification Defaults #
*** 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._response_buffer = self._response_buffer + text + "\n"self.__noMailCmdResponse = 0 def AddToResponse(self, text):
*** 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)

some further developments have taken place:
the subscribe cgi now uses the confirmation step instead of the old web-confirmation step when confirmations are required for subscription.
i'll post more patches and a distribution later tonight.
scott
On Wed, Apr 15, 1998 at 06:31:42AM -0400, Scott wrote: | | i have developed a working confirmation step in the subscription | process for mailman.
participants (1)
-
Scott